本文将为您提供关于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 boot 整合spring security中spring security版本升级的遇到的坑
- Spring security 5.0:spring security savedrequest 为空
- Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的
- spring security filter的工作原理
Spring Security 工作原理概览(spring security详解)
本文由读者 muggle 投稿,muggle 是一位具备极客精神的90后单身老实猿,对 Spring Security 有丰富的使用经验,muggle 个人博客地址是 https://muggle0.github.io。
Security 原理分析
SpringSecurity 过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:
WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
HeaderWriterFilter:用于将头信息加入响应中。
CsrfFilter:用于处理跨站请求伪造。
LogoutFilter:用于处理退出登录。
UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自
/login
的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为username
和password
,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
BasicAuthenticationFilter:检测和处理 http basic 认证。
RequestCacheAwareFilter:用来处理请求的缓存。
SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
SessionManagementFilter:管理 session 的过滤器
ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
FilterSecurityInterceptor:可以看做过滤器链的出口。
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
SpringSecurity 流程图
先来看下面一个 Spring Security 执行流程图,只要把 SpringSecurity 的执行过程弄明白了,这个框架就会变得很简单:
流程说明
客户端发起一个请求,进入 Security 过滤器链。
当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
Security 配置
在 WebSecurityConfigurerAdapter
这个类里面可以完成上述流程图的所有配置
配置类伪代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
.and().authorizeRequests().antMatchers("/test").hasRole("test")
.anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable();
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
}
}
配置简介
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)
这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说:
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
这是配置登录相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()代表任意用户可访问。
http
.authorizeRequests()
.antMatchers("/test").hasRole("test")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
以上配置是权限相关的配置,配置了一个 /test
url 该有什么权限才能访问, anyRequest() 表示所有请求,authenticated() 表示已登录用户才能访问, accessDecisionManager() 表示绑定在 url 上的鉴权管理器
为了对比,现在贴出另一个权限配置清单:
http.authorizeRequests()
.antMatchers("/tets_a/**","/test_b/**").hasRole("test")
.antMatchers("/a/**","/b/**").authenticated()
.accessDecisionManager(accessDecisionManager())
我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意 url 上;而且可以硬编码各种 url 权限:
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
登出相关配置,这里配置了登出 url 和登出成功处理器:
http
.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
上面代码是配置鉴权失败的处理器。
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
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:
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider()));
return authenticationManager;
}
我这里给 authenticationManager 配置了两个认证器,执行过程参考流程图。
定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明:
public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new MyExpressionVoter(),
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。
Security 权限系统
UserDetails
Security 中的用户接口,我们自定义用户类要实现该接口。
GrantedAuthority
Security 中的用户权限接口,自定义权限需要实现该接口:
public class MyGrantedAuthority implements GrantedAuthority {
private String authority;
}
authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_
前缀,比如我们的配置 authorizeRequests().antMatchers("/test").hasRole("test")
,配置了一个 test
权限但我们存储的权限字段(authority)应该是 ROLE_test
。
UserDetailsService
Security 中的用户 Service,自定义用户服务类需要实现该接口:
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return.....
}
}
loadUserByUsername的作用在上文中已经说明,就是根据用户名查询用户对象。
SecurityContextHolder
用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。
手动填充 SecurityContextHolder 示例:
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);
对于使用 token 鉴权的系统,我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。
Security 扩展
Security 可扩展的有
鉴权失败处理器
验证器
登录成功处理器
投票器
自定义token处理过滤器
登出成功处理器
登录失败处理器
自定义 UsernamePasswordAuthenticationFilter
鉴权失败处理器
Security 鉴权失败默认跳转登录页面,我们可以实现 AccessDeniedHandler 接口,重写 handle() 方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中。
验证器
实现 AuthenticationProvider 接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点。
登录成功处理器
在 Security 中验证成功默认跳转到上一次请求页面或者路径为 "/" 的页面,我们同样可以自定义:继承 SimpleUrlAuthenticationSuccessHandler 这个类或者实现 AuthenticationSuccessHandler 接口。我这里建议采用继承的方式,SimpleUrlAuthenticationSuccessHandler 是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。
投票器
投票器可继承 WebExpressionVoter 或者实现 AccessDecisionVoter
注意:投票器 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 。
这里配置略微复杂,贴一下代码清单
初始化过滤器:
MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter(redisService);
myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/sign_in");
myUsernamePasswordAuthenticationFilter.setAuthenticationManager(getAuthenticationManager());
return myUsernamePasswordAuthenticationFilter;
}
添加到配置:
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 ,那么就需要重写各个处理器了。
往期文章一览
1、工作之余,你是怎么提高技术的?
2、两年了,我写了这些干货!
3、想和大家谈一点合作
4、一个Java程序猿眼中的前后端分离以及Vue.js入门
5、跟着平台混了四年,现在要单飞了!
本文分享自微信公众号 - 江南一点雨(a_javaboy)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
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 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
中。
此处存储的请求数据为 savedRequest
。
Spring security
在用户身份验证后重定向 requestCache
和 savedRequest
对象。
SavedRequest
必须为空,因为如果不使用登录页面直接使用 URL,则前一个请求中没有请求数据。
您可以尝试从登录页面登录或使用 Referrer
标头获取页面 URL。
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的必要性是什么?我们一层一层逐步说明这个问题:
- 首先要解决的是如何在Filter中获取Spring容器中Bean对象,因为在Servlet容器中启动时,各个Filter的实例便会初始化并完成注册,此时Spring Bean对象还没有完成整个加载过程,不能直接注入,不过很容易想到,可以用一个“虚拟”的Filter在Servlet容器启动时先完成注册,然后在执行doFilter时,再获取对应的Spring Bean作为实际的Filter实例,执行具体的doFilter逻辑,这是一个典型的委派模式,Spring Security为此提供了一个名为DelegatingFilterProxy的类,下文再作详细介绍。
- 解决了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功能时,将断点设置在这个入口,方便我们跟踪定位问题等等
- FilterChainProxy作为统一收口,同时也起到了打通SecurityFilterChain的桥梁作用,在调用doFilter方法时,实际上都交给某个SecurityFilterChain实例执行,到这里请求才算是进入了我们使用HttpSecurity配置的各个Filter,而在执行SecurityFilterChain的前后位置,又可以统一添加一些处理,例如添加Spring Security的防火墙HttpFirewall,用以防范某些特定类型的攻击
- 最后还有一点,Servlet Filter本身也存在一定的局限性,例如映射配置不够灵活,只能根据URL进行匹配,而SecurityFilterChain通过RequestMatcher接口实现了不同匹配逻辑及组合,大大丰富了匹配规则映射的能力
综上所述,通过DelegatingFilterProxy->FilterChainProxy->SecurityFilterChain这样的三层结构关系,使得SecurityFilterChain中的各个Filter被当成了一个整体,置于Servlet FilterChain之中,又能和其他的Filter独立开,不论我们如何配置SecurityFilterChain,都不会引起Servlet FilterChain的变更,这样的设计很好地遵循了开放封闭原则,即对Servlet Filter的修改是保持封闭的,而对Spring Security Filter的配置和扩展是保持开放的。
其实,我们在很多Spring的框架中,都可以见到这种设计,本质上来说,即通过添加一个中间层来达到解耦的目的,我们应该深入地理解这种设计,并学以致用。
三、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官方文档的图示,可以更好地理解整个执行流程
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的工作原理的相关信息,请在本站查询。
本文标签: