GVKun编程网logo

Spring Security 工作原理概览(spring security详解)

33

本文将为您提供关于SpringSecurity工作原理概览的详细介绍,我们还将为您解释springsecurity详解的相关知识,同时,我们还将为您提供关于springboot整合springsecu

本文将为您提供关于Spring Security 工作原理概览的详细介绍,我们还将为您解释spring security详解的相关知识,同时,我们还将为您提供关于spring boot 整合spring security中spring security版本升级的遇到的坑、Spring security 5.0:spring security savedrequest 为空、Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的、spring security filter的工作原理的实用信息。

本文目录一览:

Spring Security 工作原理概览(spring security详解)

Spring Security 工作原理概览(spring security详解)

本文由读者 muggle 投稿,muggle 是一位具备极客精神的90后单身老实猿,对 Spring Security 有丰富的使用经验,muggle 个人博客地址是 https://muggle0.github.io。

Security 原理分析

SpringSecurity 过滤器链

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:

  1. WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

  2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

  3. HeaderWriterFilter:用于将头信息加入响应中。

  4. CsrfFilter:用于处理跨站请求伪造。

  5. LogoutFilter:用于处理退出登录。

  6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。

  7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

  8. BasicAuthenticationFilter:检测和处理 http basic 认证。

  9. RequestCacheAwareFilter:用来处理请求的缓存。

  10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。

  11. AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

  12. SessionManagementFilter:管理 session 的过滤器

  13. ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。

  14. FilterSecurityInterceptor:可以看做过滤器链的出口。

  15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

SpringSecurity 流程图

先来看下面一个 Spring Security 执行流程图,只要把 SpringSecurity 的执行过程弄明白了,这个框架就会变得很简单:

流程说明

  1. 客户端发起一个请求,进入 Security 过滤器链。

  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。

  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

  4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

Security 配置

WebSecurityConfigurerAdapter 这个类里面可以完成上述流程图的所有配置

配置类伪代码

  
  
  
  1. @Configuration

  2. @EnableWebSecurity

  3. public class SecurityConfig extends WebSecurityConfigurerAdapter {

  4. @Override

  5. protected void configure(AuthenticationManagerBuilder auth) throws Exception {

  6. auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());

  7. }

  8. @Override

  9. public void configure(WebSecurity web) throws Exception {

  10. web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");

  11. }

  12. @Override

  13. protected void configure(HttpSecurity http) throws Exception {

  14. http

  15. .formLogin()

  16. .loginPage("/login_page")

  17. .passwordParameter("username")

  18. .passwordParameter("password")

  19. .loginProcessingUrl("/sign_in")

  20. .permitAll()

  21. .and().authorizeRequests().antMatchers("/test").hasRole("test")

  22. .anyRequest().authenticated().accessDecisionManager(accessDecisionManager())

  23. .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())

  24. .and().csrf().disable();

  25. http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

  26. http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

  27. http.addFilterAfter(new MyFittler(), LogoutFilter.class);

  28. }

  29. }

配置简介

  • configure(AuthenticationManagerBuilder auth)

AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。

  • configure(WebSecurity web)

这个配置方法用于配置静态资源的处理方式,可使用 Ant 匹配规则。

  • configure(HttpSecurity http)

这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说:

  
  
  
  1. http

  2. .formLogin()

  3. .loginPage("/login_page")

  4. .passwordParameter("username")

  5. .passwordParameter("password")

  6. .loginProcessingUrl("/sign_in")

  7. .permitAll()

这是配置登录相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()代表任意用户可访问。

  
  
  
  1. http

  2. .authorizeRequests()

  3. .antMatchers("/test").hasRole("test")

  4. .anyRequest().authenticated()

  5. .accessDecisionManager(accessDecisionManager());

以上配置是权限相关的配置,配置了一个 /test url 该有什么权限才能访问, anyRequest() 表示所有请求,authenticated() 表示已登录用户才能访问, accessDecisionManager() 表示绑定在 url 上的鉴权管理器

为了对比,现在贴出另一个权限配置清单:

  
  
  
  1. http.authorizeRequests()

  2. .antMatchers("/tets_a/**","/test_b/**").hasRole("test")

  3. .antMatchers("/a/**","/b/**").authenticated()

  4. .accessDecisionManager(accessDecisionManager())

我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意 url 上;而且可以硬编码各种 url 权限:

  
  
  
  1. http

  2. .logout()

  3. .logoutUrl("/logout")

  4. .logoutSuccessHandler(new MyLogoutSuccessHandler())

登出相关配置,这里配置了登出 url 和登出成功处理器:

  
  
  
  1. http

  2. .exceptionHandling()

  3. .accessDeniedHandler(new MyAccessDeniedHandler());

上面代码是配置鉴权失败的处理器。

  
  
  
  1. http.addFilterAfter(new MyFittler(), LogoutFilter.class);

  2. http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore 加在对应的过滤器之前,addFilterAfter 加在对应的过滤器之后,addFilterAt 加在过滤器同一位置,事实上框架原有的 Filter 在启动 HttpSecurity 配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用 addFilterAt 方法插入的 Filter ,会在这个位置上的原有 Filter 之前执行。

注:关于 HttpSecurity 使用的是链式编程,其中 http.xxxx.and.yyyyy 这种写法和 http.xxxx;http.yyyy 写法意义一样。

  • 自定义 AuthenticationManager 和 AccessDecisionManager

重写 authenticationManagerBean() 方法,并构造一个 authenticationManager:

  
  
  
  1. @Override

  2. public AuthenticationManager authenticationManagerBean() throws Exception {

  3. ProviderManager authenticationManager = new ProviderManager(Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider()));

  4. return authenticationManager;

  5. }

我这里给 authenticationManager 配置了两个认证器,执行过程参考流程图。

定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明:

  
  
  
  1. public AccessDecisionManager accessDecisionManager(){

  2. List<AccessDecisionVoter<? extends Object>> decisionVoters

  3. = Arrays.asList(

  4. new MyExpressionVoter(),

  5. new WebExpressionVoter(),

  6. new RoleVoter(),

  7. new AuthenticatedVoter());

  8. return new UnanimousBased(decisionVoters);

  9. }

投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。

Security 权限系统

  • UserDetails

Security 中的用户接口,我们自定义用户类要实现该接口。

  • GrantedAuthority

Security 中的用户权限接口,自定义权限需要实现该接口:

  
  
  
  1. public class MyGrantedAuthority implements GrantedAuthority {

  2. private String authority;

  3. }

authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_ 前缀,比如我们的配置 authorizeRequests().antMatchers("/test").hasRole("test"),配置了一个 test 权限但我们存储的权限字段(authority)应该是 ROLE_test

  • UserDetailsService

Security 中的用户 Service,自定义用户服务类需要实现该接口:

  
  
  
  1. @Service

  2. public class MyUserDetailService implements UserDetailsService {

  3. @Override

  4. public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

  5. return.....

  6. }

  7. }

loadUserByUsername的作用在上文中已经说明,就是根据用户名查询用户对象。

  • SecurityContextHolder

用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。

手动填充 SecurityContextHolder 示例:

  
  
  
  1. UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);

  2. SecurityContextHolder.getContext().setAuthentication(token);

对于使用 token 鉴权的系统,我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。

Security 扩展

Security 可扩展的有

  1. 鉴权失败处理器

  2. 验证器

  3. 登录成功处理器

  4. 投票器

  5. 自定义token处理过滤器

  6. 登出成功处理器

  7. 登录失败处理器

  8. 自定义 UsernamePasswordAuthenticationFilter

  • 鉴权失败处理器

Security 鉴权失败默认跳转登录页面,我们可以实现 AccessDeniedHandler 接口,重写 handle() 方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中。

  • 验证器

实现 AuthenticationProvider 接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点。

  • 登录成功处理器

在 Security 中验证成功默认跳转到上一次请求页面或者路径为 "/" 的页面,我们同样可以自定义:继承 SimpleUrlAuthenticationSuccessHandler 这个类或者实现 AuthenticationSuccessHandler 接口。我这里建议采用继承的方式,SimpleUrlAuthenticationSuccessHandler 是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。

  • 投票器

投票器可继承 WebExpressionVoter 或者实现 AccessDecisionVoter 接口;WebExpressionVoter 是 Security 默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 上文;

注意:投票器 vote 方法返回一个int值;-1代表反对,0代表弃权,1代表赞成;投票管理器收集投票结果,如果最终结果大于等于0则放行该请求。

  • 自定义token处理过滤器

自定义 token 处理器继承自 OncePerRequestFilter 或者 GenericFilterBean 或者 Filter 都可以,在这个处理器里面需要完成的逻辑是:获取请求里的 token,验证 token 是否合法然后填充 SecurityContextHolder ,虽然说过滤器只要添加在投票器之前就可以,但我这里还是建议添加在 http.addFilterAfter(new MyFittler(), LogoutFilter.class);

  • 登出成功处理器

实现LogoutSuccessHandler接口,添加到配置的方式参考上文。

  • 登录失败处理器

登录失败默认跳转到登录页,我们同样可以自定义。继承 SimpleUrlAuthenticationFailureHandler 或者实现 AuthenticationFailureHandler,建议采用继承。

  • 自定义UsernamePasswordAuthenticationFilter

我们自定义UsernamePasswordAuthenticationFilter可以极大提高我们 Security的灵活性(比如添加验证验证码是否正确的功能)。

我们直接继承 UsernamePasswordAuthenticationFilter ,然后在配置类中初始化这个过滤器,给这个过滤器添加登录失败处理器,登录成功处理器,登录管理器,登录请求 url 。

这里配置略微复杂,贴一下代码清单

初始化过滤器:

  
  
  
  1. MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){

  2. MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter(redisService);

  3. myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());

  4. myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());

  5. myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/sign_in");

  6. myUsernamePasswordAuthenticationFilter.setAuthenticationManager(getAuthenticationManager());

  7. return myUsernamePasswordAuthenticationFilter;

  8. }

添加到配置:

  
  
  
  1. http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

总结

对于 Security 的扩展配置关键在于 configure(HttpSecurityhttp) 方法;扩展认证方式可以自定义 authenticationManager 并加入自己验证器,在验证器中抛出异常不会终止验证流程;扩展鉴权方式可以自定义 accessDecisionManager 然后添加自己的投票器并绑定到对应的 url(url 匹配方式为 ant)上,投票器 vote(Authenticationauthentication,FilterInvocationfi,Collection<ConfigAttribute>attributes) 方法返回值为三种:-1 0 1,分别表示反对弃权赞成。

对于 token 认证的校验方式,可以暴露一个获取的接口,或者重写 UsernamePasswordAuthenticationFilter 过滤器和扩展登录成功处理器来获取 token,然后在 LogoutFilter 之后添加一个自定义过滤器,用于校验和填充 SecurityContextHolder。

另外,Security 的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回 json ,那么就需要重写各个处理器了。


关注牧码小子,后台回复 Java ,领取松哥为你精心准备的Java干货!
 

往期文章一览

1、工作之余,你是怎么提高技术的?

2、两年了,我写了这些干货!

3、想和大家谈一点合作

4、一个Java程序猿眼中的前后端分离以及Vue.js入门

5、跟着平台混了四年,现在要单飞了!

你点的每个在看,我都认真当成了喜欢


本文分享自微信公众号 - 江南一点雨(a_javaboy)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

spring boot 整合spring security中spring security版本升级的遇到的坑

spring boot 整合spring security中spring security版本升级的遇到的坑

在spring security3.x的版本中

hasAnyRole这个方法不会对我们需要认证的身份前面加个前缀ROLE_,在3.x版本hasRole的源码如下

 

public final boolean hasAnyRole(String... roles) {
        Set<String> roleSet = getAuthoritySet();

        for (String role : roles) {
            if (roleSet.contains(role)) {
                return true;
            }
        }

        return false;
    }

而4.x版本下的会根据我的具体情况看看是不是要加前缀,代码如下

public final boolean hasAnyRole(String... roles) {
        return hasAnyAuthorityName(defaultRolePrefix, roles);
    }

    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = getAuthoritySet();

        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
    }
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        }
        if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
            return role;
        }
        if (role.startsWith(defaultRolePrefix)) {
            return role;
        }
        return defaultRolePrefix + role;
    }

这里的这个坑要小心,如果加了Role_前缀,那么你登录时的角色的前面也需要加Role_才行,不然会认证失败,然后就是403页面了。(ps:hasRole这个方法也有这个版本问题,具体也是和hasAnyRole差不多。)

Spring security 5.0:spring security savedrequest 为空

Spring security 5.0:spring security savedrequest 为空

如何解决Spring security 5.0:spring security savedrequest 为空?

我有一个 spring boot 客户端应用程序,我在其中使用 spring-boot-starter-oauth2-client 和 spring-boot-starter-security。同一个应用程序在一个环境中运行良好,但是在另一个环境中部署后,我可以看到 spring security 保存的请求为空,这就是它不断重定向到登录页面的原因。

启用 spring 安全调试日志后,我可以看到身份验证成功并检索到用户详细信息。但是,它继续重定向到“/”,因为spring security 保存的请求为空。

我不知道问题出在哪里,为什么它能够在一种环境中而不是在另一种环境中保存请求。我应该从哪里开始寻找?任何帮助将不胜感激。

应用程序未重定向的调试日志

{"timestamp":"2021-05-12T17:24:40.918+10:00","app":"my-protected-application","logLevel":"INFO","thread":"http-nio-8080-exec-1","eventSource":"org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]","message":"Initializing Spring dispatcherServlet ''dispatcherServlet''"}
{"timestamp":"2021-05-12T17:24:41.104+10:00","traceId":"2efe5d4e6d04f787","spanId":"2efe5d4e6d04f787","logLevel":"DEBUG","eventSource":"org.springframework.security.web.util.matcher.AntPathRequestMatcher","message":"Checking match of request : ''/actuator/health/readiness''; against ''/actuator/info''"}
{"timestamp":"2021-05-12T17:24:41.112+10:00","message":"Checking match of request : ''/actuator/health/readiness''; against ''/actuator/health/readiness''"}
{"timestamp":"2021-05-12T17:24:41.112+10:00","eventSource":"org.springframework.security.web.FilterChainProxy","message":"/actuator/health/readiness has an empty filter list"}
{"timestamp":"2021-05-12T17:24:55.666+10:00","traceId":"9681ad4d267d4beb","spanId":"9681ad4d267d4beb","thread":"http-nio-8080-exec-2","message":"Checking match of request : ''/oauth2/authorization/my-protected-application''; against ''/actuator/info''"}
{"timestamp":"2021-05-12T17:24:55.666+10:00","message":"Checking match of request : ''/oauth2/authorization/my-protected-application''; against ''/actuator/health/readiness''"}
{"timestamp":"2021-05-12T17:24:55.666+10:00","message":"Checking match of request : ''/oauth2/authorization/my-protected-application''; against ''/actuator/health/liveness''"}
{"timestamp":"2021-05-12T17:24:55.668+10:00","message":"/oauth2/authorization/my-protected-application at position 1 of 14 in additional filter chain; firing Filter: ''WebAsyncManagerIntegrationFilter''"}
{"timestamp":"2021-05-12T17:24:55.670+10:00","message":"/oauth2/authorization/my-protected-application at position 2 of 14 in additional filter chain; firing Filter: ''SecurityContextPersistenceFilter''"}
{"timestamp":"2021-05-12T17:24:55.670+10:00","eventSource":"org.springframework.security.web.context.HttpSessionSecurityContextRepository","message":"No HttpSession currently exists"}
{"timestamp":"2021-05-12T17:24:55.670+10:00","message":"No SecurityContext was available from the HttpSession: null. A new one will be created."}
{"timestamp":"2021-05-12T17:24:55.673+10:00","message":"/oauth2/authorization/my-protected-application at position 3 of 14 in additional filter chain; firing Filter: ''HeaderWriterFilter''"}
{"timestamp":"2021-05-12T17:24:55.674+10:00","message":"/oauth2/authorization/my-protected-application at position 4 of 14 in additional filter chain; firing Filter: ''logoutFilter''"}
{"timestamp":"2021-05-12T17:24:55.674+10:00","eventSource":"org.springframework.security.web.util.matcher.OrRequestMatcher","message":"Trying to match using Ant [pattern=''/logout'',GET]"}
{"timestamp":"2021-05-12T17:24:55.675+10:00","message":"Checking match of request : ''/oauth2/authorization/my-protected-application''; against ''/logout''"}
{"timestamp":"2021-05-12T17:24:55.675+10:00",POST]"}
{"timestamp":"2021-05-12T17:24:55.675+10:00","message":"Request ''GET /oauth2/authorization/my-protected-application'' doesn''t match ''POST /logout''"}
{"timestamp":"2021-05-12T17:24:55.675+10:00",PUT]"}
{"timestamp":"2021-05-12T17:24:55.675+10:00","message":"Request ''GET /oauth2/authorization/my-protected-application'' doesn''t match ''PUT /logout''"}
{"timestamp":"2021-05-12T17:24:55.675+10:00",DELETE]"}
{"timestamp":"2021-05-12T17:24:55.675+10:00","message":"Request ''GET /oauth2/authorization/my-protected-application'' doesn''t match ''DELETE /logout''"}
{"timestamp":"2021-05-12T17:24:55.675+10:00","message":"No matches found"}
{"timestamp":"2021-05-12T17:24:55.676+10:00","message":"/oauth2/authorization/my-protected-application at position 5 of 14 in additional filter chain; firing Filter: ''OAuth2AuthorizationRequestRedirectFilter''"}
{"timestamp":"2021-05-12T17:24:55.676+10:00","message":"Checking match of request : ''/oauth2/authorization/my-protected-application''; against ''/oauth2/authorization/{registrationId}''"}
{"timestamp":"2021-05-12T17:24:55.676+10:00","message":"Checking match of request : ''/oauth2/authorization/my-protected-application''; against ''/oauth2/authorization/{registrationId}''"}

解决方法

您是直接调用登录 URL 而不是尝试通过登录页面登录吗?

用户请求的请求数据存储在 requestCache 中。 此处存储的请求数据为 savedRequestSpring security 在用户身份验证后重定向 requestCachesavedRequest 对象。

SavedRequest 必须为空,因为如果不使用登录页面直接使用 URL,则前一个请求中没有请求数据。

您可以尝试从登录页面登录或使用 Referrer 标头获取页面 URL。

Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的

Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的

上一篇主要介绍了Spring Secuirty中的过滤器链SecurityFilterChain是如何配置的,那么在配置完成之后,SecurityFilterChain是如何在应用程序中调用各个Filter,从而起到安全防护的作用,本文主要围绕SecurityFilterChain的工作原理做详细的介绍。

一、Filter背景知识

因为Spring Security底层依赖Servlet的过滤器技术,所以先简单地回顾一下相关背景知识。
过滤器Filter是Servlet的标准组件,自Servlet 2.3版本引入,主要作用是在Servlet实例接受到请求之前,以及返回响应之后,这两个方向上进行动态拦截,这样就可以与Servlet主业务逻辑解耦,从而实现灵活性和可扩展性,利用这个特性可以实现很多功能,例如身份认证,统一编码,数据加密解密,审计日志等等。
Filter接口定义了3个方法:doFilter,init和destory,其中doFilter就是请求进入过滤器时需要执行的逻辑,伪代码实现如下

public class ExampleFilter implements Filter {
    …
    public void doFilter(ServletRequest request, ServletResponse response,
                            FilterChain chain) throws IOException, ServletException {
        doSomething();
        chain.doFilter(request,response);    
    }
    …
}

其中FilterChain中维护了一个所有已注册的过滤器数组,它组成了真正的“过滤器链”,下面是FilterChain的实现类ApplicationFilterChain的部分源码:当请求到达Servlet容器时,就会创建出一个FilterChain实例,然后调用FilterChain#doFilter方法,这时会从数组中取出下一个过滤器,并调用Filter#doFilter方法,在方法末尾又会将请求继续交由FilterChain处理,如此往复,从而实现职责链模式的调用方式。

private void internalDoFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

    // Call the next filter if there is one
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();
            ...
            if (Globals.IS_SECURITY_ENABLED) {
               // ...
            } else {
                filter.doFilter(request, response, this);
            }
        } catch (...) {
          ...
        }
        return;
    }

    // We fell off the end of the chain -- call the servlet instance
    try {
        ...
        // Use potentially wrapped request from this point
        if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) &&
                Globals.IS_SECURITY_ENABLED) {
           ...
        } else {
            servlet.service(request, response);
        }
    } catch (...) {
       ...
    } finally {
       ...
    }
}

Filter实例可以在web.xml中注册,同时设置URL映射逻辑,当URL符合设置的规则时,便会进入该Filter,举个例子,在Spring Boot问世之前开发一个普通的Spring MVC应用时,经常会配置一个CharacterEncodingFilter,用于统一请求和响应的编码,以避免一些中文乱码的问题

<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern> <!-- 相当于拦截所有请求 -->
</filter-mapping>

二、SecurityFilterChain的必要性

再回到SecurityFilterChain,先来思考一个问题:基于上面所介绍的Filter,我们自然会想到,定义一系列与安全相关的Filter,例如我们在上一篇提到的那些包括认证,鉴权等在内的Filter,然后只要把他们一个个注册到FilterChain中,就可以实现各种安全特性,看起来也并不需要Spring Security提供的SecuriyFilterChain,也正因如此,初学者经常会有一个疑问,就是明明加一个Filter就可以解决的事,为什么搞得这么复杂?
那么SecurityFilterChain的必要性是什么?我们一层一层逐步说明这个问题:

  1. 首先要解决的是如何在Filter中获取Spring容器中Bean对象,因为在Servlet容器中启动时,各个Filter的实例便会初始化并完成注册,此时Spring Bean对象还没有完成整个加载过程,不能直接注入,不过很容易想到,可以用一个“虚拟”的Filter在Servlet容器启动时先完成注册,然后在执行doFilter时,再获取对应的Spring Bean作为实际的Filter实例,执行具体的doFilter逻辑,这是一个典型的委派模式,Spring Security为此提供了一个名为DelegatingFilterProxy的类,下文再作详细介绍。
  2. 解决了Spring Bean容器与Servlet Filter整合的问题之后,我们是否可以将每一个Filter都通过DelegatingFilterProxy的模式添加到FilterChain中?试想一下,如果每个Spring Security的Filter都分别创建一个独立的委派类,那么通过ApplicationContext查找bean的代码就会反复出现,这在很大程度上违背了依赖注入的原则,也极大了增加了维护成本和开发成本,为了解决这个问题,在上述DelegateFilterProxy基础上,Spring Security又引入了一个代理类FilterChainProxy,它可以看作是Spring Security Filter的统一入口,此时,从Servlet的FIlterChain角度来看,整个Spring Security只定义了一个Filter,即DelegatingFilterProxy,而执行doFilter时则委派给了FilterChainProxy,这样就可以利用这个入口简化很多工作,例如官方文档中提到,可以在调试Spring Security功能时,将断点设置在这个入口,方便我们跟踪定位问题等等
  3. FilterChainProxy作为统一收口,同时也起到了打通SecurityFilterChain的桥梁作用,在调用doFilter方法时,实际上都交给某个SecurityFilterChain实例执行,到这里请求才算是进入了我们使用HttpSecurity配置的各个Filter,而在执行SecurityFilterChain的前后位置,又可以统一添加一些处理,例如添加Spring Security的防火墙HttpFirewall,用以防范某些特定类型的攻击
  4. 最后还有一点,Servlet Filter本身也存在一定的局限性,例如映射配置不够灵活,只能根据URL进行匹配,而SecurityFilterChain通过RequestMatcher接口实现了不同匹配逻辑及组合,大大丰富了匹配规则映射的能力

综上所述,通过DelegatingFilterProxy->FilterChainProxy->SecurityFilterChain这样的三层结构关系,使得SecurityFilterChain中的各个Filter被当成了一个整体,置于Servlet FilterChain之中,又能和其他的Filter独立开,不论我们如何配置SecurityFilterChain,都不会引起Servlet FilterChain的变更,这样的设计很好地遵循了开放封闭原则,即对Servlet Filter的修改是保持封闭的,而对Spring Security Filter的配置和扩展是保持开放的。
其实,我们在很多Spring的框架中,都可以见到这种设计,本质上来说,即通过添加一个中间层来达到解耦的目的,我们应该深入地理解这种设计,并学以致用。

image.png

三、SecuriyFilterChain的工作原理

讨论完SecurityFilterChain必要性,再来介绍SecurityFilterChain的工作原理就会变得比较好理解了

1. 注册DelegatingFilterProxy

在非Spring Boot环境可以通过web.xml进行注册,配置如下:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

而在Spring Boot环境下,则是通过RegistrationBean的方式注册Servlet组件,具体实现类为DelegatingFilterProxyRegistrationBean,它由SecurityFilterAutoConfiguration配置类创建出来,并在Servlet容器启动的时候完成Filter的注册。
完成注册后,当Servlet容器启动时,FilterChain就包含了DelegatingFilterProxy这个Filter。

2.委派FilterChainProxy

上文提到在执行DelegatingFilterProxy的doFilter方法时,实际上都是交给FilterChainProxy来执行,它是由Spring容器托管的bean对象,通过下面WebSecurityConfiguration配置类源码可以看到,其中定义了一个名称为“springSecurityFilterChain”的Bean,而其中webSecurity#build方法返回的就是FilerChainProxy的实例,其构建过程和上一篇介绍的HttpSecurity类似,这里就不再展开。

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) // "springSecurityFilterChain"
public Filter springSecurityFilterChain() throws Exception {
    ...
    return this.webSecurity.build();
}

委派过程比较简单,下面是DelegatingFilterProxy#doFilter方法的源码(可以忽略并发控制的代码),当请求进入doFilter之后,首先调用initDelegate方法,这里利用Spring的ApplicationContext#getBean方法获取名为“springSecurityFilterChain“的bean对象,即FilterChainProxy,然后调用其doFilter方法,这样就完成了委派调用。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
       throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
       synchronized (this.delegateMonitor) {
          delegateToUse = this.delegate;
          if (delegateToUse == null) {
             WebApplicationContext wac = findWebApplicationContext();
             if (wac == null) {
                throw new IllegalStateException("No WebApplicationContext found: " +
                      "no ContextLoaderListener or DispatcherServlet registered?");
             }
             delegateToUse = initDelegate(wac);
          }
          this.delegate = delegateToUse;
       }
    }

    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    String targetBeanName = getTargetBeanName(); // "springSecurityFilterChain"
    Assert.state(targetBeanName != null, "No target bean name set");
    Filter delegate = wac.getBean(targetBeanName, Filter.class);
    if (isTargetFilterLifecycle()) {
       delegate.init(getFilterConfig());
    }
    return delegate;
}

protected void invokeDelegate(
       Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
       throws ServletException, IOException {

    delegate.doFilter(request, response, filterChain);
}

3. 执行SecurityFilterChain的过滤器链

严格来说,最终执行doFilter的并不是SecuritFilterChain,FilterChainProxy内部维护了一个SecurityFilterChain的List列表,在调用doFilter方法时,会根据SecurityFilterChain#match方法匹配的结果决定选择某一个SecurityFilterChain,然后取出该SecurityFilterChain所有的Filter,用其构造一个VirtualFilterChain,这才是实际意义上过滤器链执行的入口。

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
       throws IOException, ServletException {
    FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
    List<Filter> filters = getFilters(firewallRequest); // 重点关注这个方法,获取到某个SecurityFilterChain的所有Filter
    if (filters == null || filters.size() == 0) {
        ...
       firewallRequest.reset();
       this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
       return;
    }
     ...
    FilterChain reset = (req, res) -> {
          ...
       // Deactivate path stripping as we exit the security filter chain
       firewallRequest.reset();
       chain.doFilter(req, res);
    };
    // 装饰器模式,实际上返回了VirtualFilterChain的实例
    this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}

private List<Filter> getFilters(HttpServletRequest request) {
    int count = 0;
    for (SecurityFilterChain chain : this.filterChains) {
         ...
       if (chain.matches(request)) {
          return chain.getFilters();
       }
    }
    return null;
}


public FilterChain decorate(FilterChain original, List<Filter> filters) {
    return new VirtualFilterChain(original, filters);
}

VirtualFilterChain的实现也并不复杂,其doFilter方法源码如下,原理和Servlet的FilterChain的实现类ApplicationFilterChain基本类似,不过当所有Filter都执行完之后,它会交给originalChain继续执行,即回到Servlet的FilterChain。上文提到,如果要打断点debug,这里是一个比较好的位置,可以看到Spring Security中定义各个Filter执行的过程。

@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    if (this.currentPosition == this.size) {
       this.originalChain.doFilter(request, response);
       return;
    }
    this.currentPosition++;
    Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
    if (logger.isTraceEnabled()) {
       String name = nextFilter.getClass().getSimpleName();
       logger.trace(LogMessage.format("Invoking %s (%d/%d)", name, this.currentPosition, this.size));
    }
    nextFilter.doFilter(request, response, this);
}

最后,再结合Spring Security官方文档的图示,可以更好地理解整个执行流程

image.png

spring security filter的工作原理

spring security filter的工作原理

这篇文章介绍filter的工作原理。配置方式为xml。

Filter如何进入执行逻辑的

初始配置:

 <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

DelegatingFilterProxy这个类继承了GenericFilterBean,GenericFilterBean实现了Filter接口。

这个配置是一切的开始,配置完这个之后,在启动项目的时候会执行Filterd的初始化方法:

@Override
    public final void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing filter ''" + filterConfig.getFilterName() + "''");
        }

        this.filterConfig = filterConfig;

        // Set bean properties from init parameters.
        PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
                Environment env = this.environment;
                if (env == null) {
                    env = new StandardServletEnvironment();
                }
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            }
            catch (BeansException ex) {
                String msg = "Failed to set bean properties on filter ''" +
                    filterConfig.getFilterName() + "'': " + ex.getMessage();
                logger.error(msg, ex);
                throw new NestedServletException(msg, ex);
            }
        }

        // Let subclasses do whatever initialization they like.
        initFilterBean(); // 这个方法

        if (logger.isDebugEnabled()) {
            logger.debug("Filter ''" + filterConfig.getFilterName() + "'' configured successfully");
        }
    }

在初始化方法中,会执行初始化Filter的方法initFilterBean。这个方法的实现在DelegatingFilterProxy中:

protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                if (this.targetBeanName == null) {
                    this.targetBeanName = getFilterName();
                }
                // Fetch Spring root application context and initialize the delegate early,
                // if possible. If the root application context will be started after this
                // filter proxy, we''ll have to resort to lazy initialization.
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac); //这个方法
                }
            }
        }
    }

在这个初始化方法中又调用initDelegate方法进行初始化:

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        String targetBeanName = getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

在这个方法中,先获取targetBeanName,这个名字是构造方法中赋值的:

public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) {
        Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty");
        this.setTargetBeanName(targetBeanName);
        this.webApplicationContext = wac;
        if (wac != null) {
            this.setEnvironment(wac.getEnvironment());
        }
    }

这个名字就是web.xml中配置的名字springSecurityFilterChain:

 <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

springSecurityFilterChain是固定不能改的,如果改了启动时就会报错,这是spring 启动时内置的一个bean,这个bean实际是FilterChainProxy。

这样一个Filter就初始化话好了,过滤器chain也初始化好了。

当一个请求进来的时候,会进入FilterChainProxy执行doFilter方法:

public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (clearContext) {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                doFilterInternal(request, response, chain);
            }
            finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        }
        else {
            doFilterInternal(request, response, chain);
        }
    }

先获取所有的Filter,然后执行doFilterInternal方法:

private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);

        List<Filter> filters = getFilters(fwRequest);

        if (filters == null || filters.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? " has no matching filters"
                                : " has an empty filter list"));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }

        // 最终执行下面的这些代码
        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }

VirtualFilterChain是一个匿名内部类:

private static class VirtualFilterChain implements FilterChain {
        private final FilterChain originalChain;
        private final List<Filter> additionalFilters;
        private final FirewalledRequest firewalledRequest;
        private final int size;
        private int currentPosition = 0;

        private VirtualFilterChain(FirewalledRequest firewalledRequest,
                FilterChain chain, List<Filter> additionalFilters) {
            this.originalChain = chain;
            this.additionalFilters = additionalFilters;
            this.size = additionalFilters.size();
            this.firewalledRequest = firewalledRequest;
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " reached end of additional filter chain; proceeding with original chain");
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                currentPosition++;

                Filter nextFilter = additionalFilters.get(currentPosition - 1);

                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " at position " + currentPosition + " of " + size
                            + " in additional filter chain; firing Filter: ''"
                            + nextFilter.getClass().getSimpleName() + "''");
                }

                nextFilter.doFilter(request, response, this);
            }
        }
    }

filter集合执行的逻辑在VirtualFilterChain的doFilter方法中。

filter是如何执行的

上面说了怎么才能进入filter的执行逻辑,下面说一下filter到底怎么执行,为什么一个
在VirtualFilterChain的doFilter方法可以执行所有的filter。

下面写一个例子,模拟filter的执行逻辑。
定义FilterChain接口、Filter接口:

public interface Filter {

    void doFilter(String username, int age, FilterChain filterChain);
}
public interface FilterChain {

    void doFilter(String username, int age);
}

定义两个Filter实现:

public class NameFilter implements Filter {

    @Override
    public void doFilter(String username, int age, FilterChain filterChain) {

        username = username + 1;
        System.out.println("username: " + username + "   age: " + age);
        System.out.println("正在执行:NameFilter");
        filterChain.doFilter(username, age);
    }
}
public class AgeFilter implements Filter {

    @Override
    public void doFilter(String username, int age, FilterChain filterChain) {

        age += 10;
        System.out.println("username: " + username + "   age: " + age);
        System.out.println("正在执行:AgeFilter");
        filterChain.doFilter(username, age);
    }
}

定义一个FilterChain实现:

public class FilterChainProxy implements FilterChain {


    private int position = 0;
    private int size = 0;
    private List<Filter> filterList = new ArrayList<>();

    public void addFilter(Filter filter) {

        filterList.add(filter);
        size++;
    }

    @Override
    public void doFilter(String username, int age) {

        if (size == position) {
            System.out.println("过滤器链执行结束");
        } else {

            Filter filter = filterList.get(position);
            position++;
            filter.doFilter(username, age, this);
        }
    }
}

测试Filter实现:

public class FilterTest {

    public static void main(String[] args) {

        FilterChainProxy proxy = new FilterChainProxy();
        proxy.addFilter(new NameFilter());
        proxy.addFilter(new AgeFilter());

        proxy.doFilter("张三", 0);
    }
}
=======
username: 张三1   age: 0
正在执行:NameFilter
username: 张三1   age: 10
正在执行:AgeFilter
过滤器链执行结束

在这个执行逻辑中,最重要的是【this】,this就是初始化的好的FilterChain实例,在这个测试实例中,this就是FilterChainProxy。

执行FilterChainProxy的doFilter方法的时候,传入了初始参数username和age,进入这个方法后,根据position取出相应的Filter,初次进入position是0,执行Filter的doFilter方法,注意,此时Filter的doFilter方法额外传入了一个this参数,这个参数就是初始化的好的FilterChain实例,在Filter中的doFilter的方法中最后又会执行FilterChain的doFilter方法,相当于第二次调用FilterChain实例的doFilter方法,此时posotion是1,然后再执行Filter的doFilter方法,直到所有的Filter执行完,整个执行过程结束。

VirtualFilterChain的doFilter方法的执行逻辑和这个测试实例中的执行逻辑基本一致。
这样就完成了整个过滤器链的执行。

总结

以前用Filter的时候就非常疑惑过滤器怎么执行的,直到今天才算解决了这个疑惑。

我们今天的关于Spring Security 工作原理概览spring security详解的分享已经告一段落,感谢您的关注,如果您想了解更多关于spring boot 整合spring security中spring security版本升级的遇到的坑、Spring security 5.0:spring security savedrequest 为空、Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的、spring security filter的工作原理的相关信息,请在本站查询。

本文标签:

上一篇Docker 入门及安装 [Docker 系列 - 1](docker安装教程)

下一篇Spring Boot 中的静态资源到底要放在哪里?(spring boot 静态资源路径配置)