零、前语
上一篇文章讲解了SpringSecurity根底装备,这一篇文章企图从源码视点来剖析SpringSecurity的工作流程。
上一篇文章运用的是spirng boot 2.7.0,这篇文章离上一篇文章比较久了,最新版也更新到spirng boot 3.1.5,版别跨度比较大,可是上一篇文章中的新版别装备在3.1.5版别中任然是有用的,只是部分装备有点小改变。所以不必忧虑版别不相同而不能运用了。文章最后会给出一版根据spirng boot 3.1.5版别(关于spring security 6.1.5)的装备,能够相互比较一下。
一、spring security过滤器
最简单的运用spring security的办法便是引进相应的spring boot security依靠,这样拜访任何接口都需求认证了。这是为什么呢?
咱们都知道,spring mvc中,一个恳求都是阅历一系列Filter,然后抵达DispatcherServlet进行恳求处理,而DispatcherServlet是不触及恳求认证的,所以认证进程必定产生在前面的Filter中,也便是说必定存在某些操作往咱们的体系中增加了过滤器,导致咱们的恳求需求认证。
咱们先来知道下spring security中界说的过滤器,也能够说是官方供给的Filter。
1.1 过滤器
spring security供给的一切过滤器都在FilterOrderRegistration类中声明,越先界说的Filter优先级越高,其间任意两个过滤器之间的优先级顺序相差100。里边运用全限定类名增加的Filter标明该类不在spring-security-config包及其依靠包中,需求单独引进。
FilterOrderRegistration() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(DisableEncodeUrlFilter.class, order.next());
put(ForceEagerSessionCreationFilter.class, order.next());
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextHolderFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
讲到过滤器,在这儿就先说下HttpSecurity中addFilter()和addFilterBefore()等办法的区别:
-
addFilter():往过滤器链中增加一个过滤器,增加的过滤器有必要坐落FilterOrderRegistration中界说的过滤器中,也便是官方界说的Filter。 -
addFilterBefore()、addFilterAfter()、addFilterAtOffsetOf()等办法:用于增加自界说过滤器,并指定过滤器优先级(在官方界说的哪个Filter前履行,在哪个Filter后履行)。当然也可所以官方界说的过滤器,假如增加的是官方界说的Filter,这儿指定的优先级不会生效,仍然是官方界说的那个优先级。至于原因,能够去看下源码。一切这儿引荐这类办法只用来增加自界说过滤器,当然,你要弄理解是怎么回事了,怎么用都行。
1.2 过滤器阻拦进程
上面界说了一系列的Filter,其间ExceptionTranslationFilter是用来处理反常的Filter,而ExceptionTranslationFilter和AuthorizationFilter是来校验恳求是否经过认证的。ExceptionTranslationFilter在spring security 6.0中被标记为过期了,在spring security 7.0中会被删除,故后续都以AuthorizationFilter进行阐明。
这儿ExceptionTranslationFilter先于AuthorizationFilter履行,这样只需再Filter履行的最外层加一个try catch就能捕获AuthorizationFilter履行的反常,假如恳求未认证,则抛出AccessDeniedException反常,由ExceptionTranslationFilter进行处理,然后重定向到登录页面。
下面咱们具体来看下ExceptionTranslationFilter完结。
1.2.1 ExceptionTranslationFilter
能够看到,完结很简单,便是在chain.doFilter(request, response)外面包了一层try catch,然后对反常进行处理。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 直接持续履行后续的Filter,这儿便是履行AuthorizationFilter
chain.doFilter(request, response);
}
// AuthorizationFilter 履行抛出IO反常,不处理,原样抛出
catch (IOException ex) {
throw ex;
}
// 不是IO反常,则进行反常处理
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
// 处理反常
handleSpringSecurityException(request, response, chain, securityException);
}
}
handleSpringSecurityException()
该办法进一步区分了反常类型,并根据不同的反常类型做不同的逻辑处理,这儿分为两类:
-
AuthenticationException:表明认证反常,包含用户不存在反常(UsernameNotFoundException)、暗码过错反常(UsernameNotFoundException)、账号过期反常(UsernameNotFoundException)反常。一切的AuthenticationException反常如下所示: -
AccessDeniedException:表明拜访被拒绝反常,也便是无权限。分为两种状况:一种是未认证,需求认证;另一种是认证了,可是不具备对资源的拜访权限。
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
handleAuthenticationException()
该办法是对AuthenticationException反常的处理
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
this.logger.trace("Sending to authentication entry point since authentication failed", exception);
sendStartAuthentication(request, response, chain, exception);
}
sendStartAuthentication()
该办法用来进行从头认证。包含认证反常AuthenticationException反常以及AccessDeniedException反常里的未认证的状况,都会调用该办法,进行从头认证。首要做了一下三件事:
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// 1、设置空的安全上下文
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextHolderStrategy.setContext(context);
// 2、保存恳求,这样登录后能重定向到登录前拜访的地址
this.requestCache.saveRequest(request, response);
// 3、认证端点处理
this.authenticationEntryPoint.commence(request, response, reason);
}
handleAccessDeniedException()
该办法用来处理AccessDeniedException反常。分两种状况处理:
- 一种是未认证,需求认证,则调用
sendStartAuthentication()办法; - 另一种是认证了,可是不具备对资源的拜访权限,则调用无权限处理器
AccessDeniedHandler进行处理
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// 判别是否为匿名用户
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
// 假如是匿名用户或许为rememberMe用户,则要求进行认证
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
// 不是匿名用户,阐明已经登录了,这个时分便是无权限了,履行无权限处理器
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
1.2.2 AuthorizationFilter
中心逻辑便是经过授权管理器进行核验,当时用户是否对当时资源有权限,假如没有权限,则抛出AccessDeniedException反常。
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1、对恳求只验证一次,并且该特点称号已经进行过验证,则不进行验证。observeOncePerRequest默以为false,表明每次恳求都要验证
if (this.observeOncePerRequest && isApplied(request)) {
chain.doFilter(request, response);
return;
}
// 2、部分恳求无需验证
if (skipDispatch(request)) {
chain.doFilter(request, response);
return;
}
// 3、缓存进行验证过的特点称号,假如observeOncePerRequest为true,则只进行一次验证,后续再次恳求时不会验证
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 4、验证,
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
// 无权限,则抛出AccessDeniedException
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
}
finally {
// 清空缓存的特点称号
request.removeAttribute(alreadyFilteredAttributeName);
}
}
二、spring security的主动装备
咱们先看下spring security的主动装备类,看他做了哪些工作:
2.1 SecurityAutoConfiguration
@AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
中心是注入了一个DefaultAuthenticationEventPublisher类型的bean,用来发布事件;然后导入了两个类SpringBootWebSecurityConfiguration、SecurityDataConfiguration,中心类是SpringBootWebSecurityConfiguration,咱们详细看下。
SpringBootWebSecurityConfiguration
完结很简单,界说了两个静态内部类。
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnClass(EnableWebSecurity.class)
@EnableWebSecurity
static class WebSecurityEnablerConfiguration {
}
}
WebSecurityEnablerConfiguration是用来判别项目中是否运用了@EnableWebSecurity注解,假如没有运用,则增加。也便是说咱们自界说spring security装备类时,能够不必增加@EnableWebSecurity注解。spring boot强壮吧!虽然可是,仍是主张自界说装备类时手动增加@EnableWebSecurity注解。
再来看下SecurityFilterChainConfiguration。注入了一个SecurityFilterChain类型的bean,该类是spring security的中心类,咱们一切的装备以及过滤器都是坐落SecurityFilterChain中
该办法做了三件事:
-
authorizeHttpRequests:装备认证过滤器AuthorizationFilter,一切恳求都会被此过滤器阻拦,假如未登录,则重定向登录页面;不然同行 -
formLogin:装备用户运用用户名暗码的登录认证过滤器UsernamePasswordAuthenticationFilter,这样咱们才干经过用户名暗码登录 -
httpBasic:装备basic认证过滤器BasicAuthenticationFilter,这样咱们就能经过增加恳求头来进行认证
虽然说spring boot主动装备只做了这三件事,可是别忘了,这儿还有一个入参HttpSecurity http,初始HttpSecurity 又是怎样的呢?
咱们都知道,装备类中@bean办法的入参来源都是容器的中一个bean目标,一切必定有一个当地注入了一个HttpSecurity类型的bean。其实便是在HttpSecurityConfiguration类中,而该类是由EnableWebSecurity注解引进的,一切呢spring security项目中需求运用EnableWebSecurity注解,也就有了前面的WebSecurityEnablerConfiguration,假如用户忘记了增加EnableWebSecurity注解,也能确保程序不犯错。看看,spring boot为用户考虑的周全吧。
2.2 HttpSecurityConfiguration
回到HttpSecurityConfiguration.httpSecurity()来,能够看到他是一个多例bean,而不是单例,这样咱们每次自界说装备时运用的HttpSecurity的装备信息都是相同的,不会因为一个当地装备了然后影响另一个当地,bean称号为org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity。
该办法首要做了以下几个装备:
- 设置认证管理器。经过注入的
AuthenticationConfiguration目标来获取AuthenticationManager,咱们也能够经过此办法来获取AuthenticationManager。 -
csrf():敞开csrf,withDefaults()是Customizer接口中的静态办法,表明不手动设置,运用默许装备,咱们也能够运用lambda进行自界说装备,后续一切withDefaults()办法都相同,就不重复阐明晰。 -
exceptionHandling():启用反常处理器,增加ExceptionTranslationFilter过滤器,咱们拜访需求认证的地址,会抛出一个反常,然后被该过滤器阻拦,重定向到登陆页面。ExceptionTranslationFilter由两个中心特点AuthenticationEntryPoint和AccessDeniedHandler,AuthenticationEntryPoint用于设置未登录用户的处理逻辑;AccessDeniedHandler用于处理用户登录成功了,可是无权限拜访的逻辑,实际运用时咱们能够设置这两个特点覆盖掉默许的操作。 -
apply(new DefaultLoginPageConfigurer<>()):增加默许的登录页的装备,该装备增加了两个过滤器:DefaultLoginPageGeneratingFilter和DefaultLogoutPageGeneratingFilter。前者用于界说默许的登录页面,后者用于界说默许的刊出页面。 -
logout(withDefaults()):增加刊出装备类(LogoutConfigurer),该装备增加了一个LogoutFilter类型的Filter,用户设置刊出恳求地址以及刊出处理器。 -
applyDefaultConfigurers():加载spring.factories文件中界说的AbstractHttpConfigurer类型的目标,关于spring boot项目再了解不过了。
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
// 结构认证管理器的构建器
AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
this.objectPostProcessor, passwordEncoder);
// 增加认证管理器
authenticationBuilder.parentAuthenticationManager(authenticationManager());
authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
// 结构HttpSecurity目标
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
// @formatter:off
// 一系列的默许装备
http
.csrf(withDefaults())
.addFilter(webAsyncManagerIntegrationFilter)
.exceptionHandling(withDefaults())
.headers(withDefaults())
.sessionManagement(withDefaults())
.securityContext(withDefaults())
.requestCache(withDefaults())
.anonymous(withDefaults())
.servletApi(withDefaults())
.apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
applyDefaultConfigurers(http);
return http;
}
2.3 源码剖析
说了这么多,咱们来看下HttpSecurity中的办法,这儿以formLogin()来进行阐明。
2.3.1 HttpSecurity.formLogin()
完结很简单,new了一个FormLoginConfigurer目标,经过getOrApply()办法增加到AbstractConfiguredSecurityBuilder.configurers集合中。入参的formLoginCustomizer是一个消费者的函数式接口,用于咱们自界说FormLoginConfigurer各个特点,假如无需自界说,则能够运用Customizer.withDefaults()
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
return HttpSecurity.this;
}
2.3.2 FormLoginConfigurer
FormLoginConfigurer继承了AbstractAuthenticationFilterConfigurer,增加了一个UsernamePasswordAuthenticationFilter过滤器。
咱们直接看init办法,至于其他特点赋值办法能够检查另一篇文章SpringSecurity根底装备 – (juejin.cn)
init()
@Override
public void init(H http) throws Exception {
super.init(http);
initDefaultLoginFilter(http);
}
调用了父类的init()办法,然后再初始化默许的登录过滤器
AbstractAuthenticationFilterConfigurer.init()
该办法首要做了三件事:
- 更新默许的认证信息
- 更新拜访权限的默许值
- 注册默许的认证端点
@Override
public void init(B http) throws Exception {
// 更新默许的认证信息
updateAuthenticationDefaults();
// 更新拜访权限的默许值。
updateAccessDefaults(http);
// 注册默许的认证端点
registerDefaultAuthenticationEntryPoint(http);
}
protected final void updateAuthenticationDefaults() {
// 设置登录恳求,默许的登录页面恳求为Get恳求的/login,故登录恳求也为/login,只是恳求办法为Post恳求
if (this.loginProcessingUrl == null) {
loginProcessingUrl(this.loginPage);
}
// 设置登录失利后的恳求地址,假如存在登录失利的Handler,则不会设置,也便是说failureHandler会覆盖掉failureUrl
if (this.failureHandler == null) {
failureUrl(this.loginPage + "?error");
}
// 设置退出登录后的重定向地址,这儿默以为欸/login?logout,表明是退出登录后重定向来的,其实便是默许的登录页
LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
}
}
protected final void registerDefaultAuthenticationEntryPoint(B http) {
// 设置ExceptionHandlingConfigurer中的默许的认证端点,其实便是设置的ExceptionTranslationFilter中的认证断点,恳求为认证时的处理逻辑
registerAuthenticationEntryPoint(http, this.authenticationEntryPoint);
}
protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {
// 从同享装备中拿到 ExceptionHandlingConfigurer
ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
}
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
getAuthenticationEntryPointMatcher(http));
}
上面提到的ExceptionHandlingConfigurer认证端点由AbstractAuthenticationFilterConfigurer结构器赋值:
protected AbstractAuthenticationFilterConfigurer() {
setLoginPage("/login");
}
private void setLoginPage(String loginPage) {
this.loginPage = loginPage;
this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}
initDefaultLoginFilter()
其实便是设置DefaultLoginPageGeneratingFilter的各个特点,特点默许值坐落DefaultLoginPageGeneratingFilter
private void initDefaultLoginFilter(H http) {
// 从同享目标中获取 DefaultLoginPageGeneratingFilter
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
// 请用表单登录
loginPageGeneratingFilter.setFormLoginEnabled(true);
// 设置用户名参数,默许username
loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
// 设置暗码参数,默许password
loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
// 设置登录页面恳求,默许/login
loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
// 设置登录失利的恳求,默以为/login?error
loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
// 设置登录恳求,默许/login
loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
}
}
FormLoginConfigurer并没有重写configure()办法,那咱们看下父类的configure()办法。
configure()
首要做了一下几件事:
- 设置端口映射器
- 设置恳求缓存器
- 设置认证管理器
- 设置认证成功处理器和认证失利处理器
- 设置session认证战略
- 设置rememberMe服务
- 设置安全上下文仓库
- 设置安全上下文持有者战略
- 增加过滤器
@Override
public void configure(B http) throws Exception {
// 1、设置端口映射器
PortMapper portMapper = http.getSharedObject(PortMapper.class);
if (portMapper != null) {
this.authenticationEntryPoint.setPortMapper(portMapper);
}
// 2、设置恳求缓存器
RequestCache requestCache = http.getSharedObject(RequestCache.class);
if (requestCache != null) {
this.defaultSuccessHandler.setRequestCache(requestCache);
}
// 3、设置认证管理器
this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 4、设置认证成功处理器和认证失利处理器
this.authFilter.setAuthenticationSuccessHandler(this.successHandler);
this.authFilter.setAuthenticationFailureHandler(this.failureHandler);
if (this.authenticationDetailsSource != null) {
this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
}
// 5、设置session认证战略
SessionAuthenticationStrategy sessionAuthenticationStrategy = http
.getSharedObject(SessionAuthenticationStrategy.class);
if (sessionAuthenticationStrategy != null) {
this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
// 6、设置rememberMe服务
RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
this.authFilter.setRememberMeServices(rememberMeServices);
}
// 7、设置安全上下文仓库
SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
SecurityContextRepository securityContextRepository = securityContextConfigurer
.getSecurityContextRepository();
this.authFilter.setSecurityContextRepository(securityContextRepository);
}
// 8、设置安全上下文持有者战略,认证经过后,会将认证信息放入这儿设置的战略中,这样咱们就能经过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到认证目标了
this.authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
// 过滤器的后置处理
F filter = postProcess(this.authFilter);
// 9、增加过滤器
http.addFilter(filter);
}
FormLoginConfigurer类的中心内容咱们就剖析完了,这儿总结下
总结
FormLoginConfigurer是一个SecurityConfigurerAdapter目标,包含其他办法如authorizeHttpRequests()增加一个AuthorizeHttpRequestsConfigurer目标;exceptionHandling()办法增加的ExceptionHandlingConfigurer目标,都是SecurityConfigurerAdapter子类。
关于一切的SecurityConfigurerAdapter类,中心办法就两个:init()和configure(),前者为初始化办法,后者为装备办法,因而咱们剖析SecurityConfigurerAdapter子类时从这两个办法下手即可,其他的都是一下特点装备办法。
这儿给出了一切SecurityConfigurerAdapter子类,这些类都会增加一个或多个Filter,用于完结对应的功能。
而关于Filter,咱们则重视doFilter()办法即可。这儿以FormLoginConfigurer增加的UsernamePasswordAuthenticationFilter为例进行进一步阐明。
2.3.3 UsernamePasswordAuthenticationFilter
咱们翻开UsernamePasswordAuthenticationFilter源码,发现其并没有doFilter()办法,可是有一个父类AbstractAuthenticationProcessingFilter,不难想到,doFilter()办法坐落其父类中。
AbstractAuthenticationProcessingFilter.doFilter
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判别恳求是否需求认证,不需求直接放行,不然需求认证
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 尝试认证
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 认证成功
// 处理session战略
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功逻辑
successfulAuthentication(request, response, chain, authenticationResult);
}
// 抛出反常,认证失利
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
attemptAuthentication()
该办法是一个笼统办法,由子类完结,这儿是UsernamePasswordAuthenticationFilter
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 获取用户名参数
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
// 获取暗码参数
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 初始化认证目标
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// 设置恳求详细信息
setDetails(request, authRequest);
// 经过认证管理器认证
return this.getAuthenticationManager().authenticate(authRequest);
}
认证管理器有许多,一般都是由ProviderManager进行处理。
ProviderManager.authenticate()
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// 身份验证供给程序逐个进行认证
for (AuthenticationProvider provider : getProviders()) {
// 当时认证器不支持,下一个
if (!provider.supports(toTest)) {
continue;
}
try {
// 认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
...... //反常处理
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
// 运用父认证器
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// 疏忽反常
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
......
// 回来认证成果
return result;
}
......
// 抛出反常
throw lastException;
}
关于根据用户名暗码的认证办法,运用的是AbstractUserDetailsAuthenticationProvider认证器,其实便是DaoAuthenticationProvider认证器
AbstractUserDetailsAuthenticationProvider.authenticate()
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 获取 UserDetails 目标,由子类DaoAuthenticationProvider完结
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
// 假如隐藏UsernameNotFoundException,则抛出BadCredentialsException,为了安全,不管是用户名仍是暗码过错,都提示暗码过错,则增加破解难度
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// 校验账号是否启用,是否确定等
this.preAuthenticationChecks.check(user);
// 校验用户名和暗码是否正确
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
// 不是缓存数据,则原样抛出反常
if (!cacheWasUsed) {
throw ex;
}
// 运用了缓存,则从头获取最新的,然后再校验
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
// 将用户目标放入缓存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创立认证成功的凭据。之前创立的认证凭据,并不代表认证成功,你能够将其理解为是一个VO目标,用来流转特点。
return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider.retrieveUser()
该办法便是调用UserDetailsService接口的loadUserByUsername(),获取UserDetails目标。咱们实际运用中不都是会界说一个类,完结UserDetailsService接口,然后在loadUserByUsername()办法中自界说处理逻辑嘛,便是这儿调用的。
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 调用UserDetailsService接口的loadUserByUsername(),获取UserDetails目标
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
successfulAuthentication()
该办法是认证成功后的处理办法,首要做了一下几件事:
- 设置安全上下文,并将其放到安全上下文持有者战略中。这样咱们才干经过
SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到上下文信息 -
rememberMe的登录成功处理逻辑 - 登录成功处理器。假如咱们自界说了,便是履行自界说的处理逻辑;假如没有自界说,则运用默许的
SimpleUrlAuthenticationSuccessHandler,将恳求重定向到指定的url,该url能够经过FormLoginConfigurer的defaultSuccessUrl()办法进行设置
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 设置安全上下文
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
// 将上下文放到安全上下文持有者战略中,这样咱们才干经过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到上下文信息
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// rememberMe 的登录成功处理逻辑
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 登录成功处理器
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
unsuccessfulAuthentication()
该办法用来履行认证失利的处理逻辑。
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// 清空上下文信息
this.securityContextHolderStrategy.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
// rememberMe服务的登录失利处理逻辑
this.rememberMeServices.loginFail(request, response);
// 履行界说的失利处理器
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
三、spring security 6.X装备
这儿给出新版别的一个装备,供我们参考。
留意不要忘了
@EnableWebSecurity注解,前面已经说过,假如忘了spring boot会主动增加,但仍是主张手动增加@EnableWebSecurity。
spring security 6.0版别和5.0的区别不大,首要便是一些办法被标记为过期了,如authorizeRequests(),一起删除了antMatchers()和mvcMatchers()办法,统一运用requestMatchers()。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
/**
* Spring Security 白名单url
*/
@Value("${security.whitelist.urls:/login,/register,/captcha}")
private String[] whileUrls;
@Resource
private RuoYiConfig ruoYiConfig;
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;
@Resource
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Resource
private MyLogoutSuccessHandler logoutSuccessHandler;
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 跨域过滤器
*/
@Resource
private CorsFilter corsFilter;
/**
* 界说暗码编码办法
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 经过 AuthenticationConfiguration 获取 AuthenticationManager
* @param configuration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
/**
* 界说spring security装备
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用csrf:根据token认证,故不需求csrf保护
.csrf(AbstractHttpConfigurer::disable)
// 禁用session:根据token认证,不需求session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 恳求认证,除了白名单外,一切恳求都要认证
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(ruoYiConfig.getWhiteUrls()).permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
// 认证失利处理操作
.exceptionHandling(exception -> exception
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
)
// 登出操作
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
// 增加jwt认证过滤器,由于JwtAuthenticationFilter是自界说过滤器,不在spring security的过滤器链中,
// 故需调用addFilterBefore或addFilterAfter办法将其加入到过滤器链中
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 增加CorsFilter过滤器,由于CorsFilter存在于spring security的过滤器链中,故直接增加即可,
// 当然也能够调用addFilterBefore(),本质上和addFilter()办法的逻辑一致。
.addFilter(corsFilter)
;
return http.build();
}
}


