SpringSecurity认证授权流程及源码分析

程序员他爱做梦 2024-02-24 02:25:37
一、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,然后对异常进行处理。

@Overridepublic 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异常里的未认证的情况,都会调用该方法,进行重新认证。主要做了一下三件事:

设置空的安全上下文保存请求,这样登录后能重定向到登录前访问的地址(使用默认配置。spring security可以配置为登录成功后重定向到指定的地址)认证端点处理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异常。

@Overridepublic 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 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 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 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()@Overridepublic void init(H http) throws Exception { super.init(http); initDefaultLoginFilter(http);}

调用了父类的init()方法,然后再初始化默认的登录过滤器

AbstractAuthenticationFilterConfigurer.init()

该方法主要做了三件事:

更新默认的认证信息更新访问权限的默认值注册默认的认证端点@Overridepublic 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服务设置安全上下文仓库设置安全上下文持有者策略添加过滤器@Overridepublic 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

@Overridepublic 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

@Overridepublic 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()

@Overridepublic 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()

@Overridepublic 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()方法中自定义处理逻辑嘛,就是这里调用的。

@Overrideprotected 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 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(); }}

作者:踏雪者链接:https://juejin.cn/post/7315846799084716083

0 阅读:2

程序员他爱做梦

简介:感谢大家的关注