一文带你读懂Spring Security 6.0的实现原理

  1. UserDetailsService:根据用户名查找用户信息的组件,默认配置的是内存存储InMemoryUserDetailsManager,你也可以配置为内置的数据库存储JdbcUserDetailsManager,但是它有很多默认的约定要遵守,对未来的扩展也不够灵活。通常会根据公司的规范要求或数据库存储的方式提供自定义的实现。

  2. PasswordEncoder:对密码进行编码的组件,建议根据公司的编码要求或当前数据库中已使用的编码来配置。如果没有特殊要求,建议采用默认的BCryptPasswordEncoder

  3. AuthenticationProvider: 为了安全需要,公司内部很多应用是不允许直接访问用户的密码的,而通常会提供一个认证的API。此时,就需要自定义AuthenticationProvider,它的核心逻辑就是调用API做认证,然后把结果再包装成Authentication返回给AuthenticationManager

  4. AuthenticationManager:它的默认实现ProviderManager适用于大部分场景,通常不需要替换,除非你不想引入太多的概念。

  5. UsernamePasswordAuthenticationFilter:如果你不想引入过多的概念和复杂度,可以提供自己的Security Filter,从而完全脱离该框架。但是需要确保认证结果模型Authentication仍然被正确处理,并且将结果通过方法SecurityContextHolder.getContext().setAuthentication放入Security Context中。

【Tips】从整个Security框架的角度来看,认证模块的核心概念只有两个,分别是认证结果AuthenticationSecurity Context。其它概念都可以认为是认证模块的内部实现细节。

鉴权模块Authorization

认证模块证明了用户的身份,但显然普通用户不应该可以随意访问管理页面或敏感资源,因此还需要有个模块来确保只有授权的用户才能执行特定的操作,这个模块称之为鉴权或者授权(Authorization)。

当你通过HttpSecurity.authorizeHttpRequests方法来配置请求的访问权限控制时,就会自动添加鉴权的Security Filter:AuthorizationFilter,它是整个SecurityFilterChain的最后一个Filter。

【版本兼容性】在Spring Security 6.0版本中,鉴权模块发生了很大变化。以前的版本中,鉴权模块使用FilterSecurityInterceptor,而6.0版本之后,这个被废弃了,取而代之的是AuthorizationFilter。同时,还有一些相关的依赖组件,如AccessDecisionManagerAccessDecisionVoter也被AuthorizationManger替换了。因此,本节的内容只限于6.0以及之后的版本。

实现原理

我们先看下鉴权模块的入口,也就是AuthorizationFilterdoFilter方法:

@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)        throws ServletException, IOException {    // ...其它非核心逻辑... //    try {        AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); // (1)        this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);        if (decision != null && !decision.isGranted()) { // (2)            throw new AccessDeniedException("Access Denied");        }        chain.doFilter(request, response);    }    finally {        request.removeAttribute(alreadyFilteredAttributeName);    }}
private Authentication getAuthentication() {    Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); // (3)    if (authentication == null) {        throw new AuthenticationCredentialsNotFoundException(                "An Authentication object was not found in the SecurityContext");    }    return authentication;}
  • 这个方法本身很简单,核心逻辑都委托给了AuthorizationManagerAuthorizationManager会校验Authentication的权限,并返回鉴权的结果AuthorizationDecision

  • 如果当前认证用户没有访问权限,就会抛出AccessDeniedException异常,表示拒绝访问。

  • 待校验的Authentication是从Security Context获取的,通常是在前面的认证阶段设置的。在这里,实际上传给AuthorizationManager的是一个获取Authentication的方法,而不是Authentication本身,这样就把实际的获取操作延后到了真正进行授权的时候,这在某些场景下可以提高性能,比如permitAll,实际上它根本用不到Authentication

AuthorizationManager

AuthorizationManager才是真正执行鉴权逻辑的类,最常用的实现类是AuthorityAuthorizationManager,它的实现逻辑很简单,它会调用AuthenticationgetAuthorities方法,获取当前登录用户的权限列表,然后将这些权限与请求需要的权限进行匹配。

实际上,选择使用哪个AuthorizationManager是开发手动设置的。我们来分析一个常用的权限配置代码片段:

@Beanstatic SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {    http.authorizeHttpRequests((requests) -> // (1)            requests                    .requestMatchers("/admin").hasAuthority("ROLE_ADMIN") // (2)                    .requestMatchers("/hello").hasRole("USER") // (3)                    .anyRequest().authenticated()); // (4)    // ... 其它配置 ... //    return http.build();}
  • 调用authorizeHttpRequests方法就相当于打开了鉴权模块,它会注册AuthorizationFilterSecurityFilterChain的最后。

  • 对于匹配/admin的请求,要求有ROLE_ADMIN权限。hasAuthority的底层就是配置了一个要求ROLE_ADMIN权限的AuthorityAuthorizationManager对象。

  • 对于匹配/hello的请求,要求有USER角色,等价于ROLE_USER权限。hasRole会自动在角色名称前面加上前缀ROLE_hasRole的底层就是配置了一个要求ROLE_USER权限的AuthorityAuthorizationManager对象。

  • 对于其它的请求,只要通过身份认证就可以访问,不需要特定的权限。类似的,authenticated方法的底层配置了一个AuthenticatedAuthorizationManager对象。

在Spring Security中,很多初学者都容易混淆RoleAuthority的区别,实际上在技术实现层面上,这两者没有本质区别,底层都仅仅是一个表示权限的字符串标识符。更多的区别在于权限管理的概念上,一般情况下,Authority表示细粒度的操作权限,比如ADD_USERDELETE_USER等,通常是动词;而Role则会与实际业务角色想对应,比如管理员ADMIN,普通员工STAFF等,通常是名称。此外,一般一个Role会对应多个Authority,同时角色之间可以存在继承关系,比如ADMIN可以继承STAFF的所有权限。

写作我们来看下hasAuthority的源码,以分析它是如何配置AuthorizeManager的:

public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) {    return access( // (3)      withRoleHierarchy( //(2)          AuthorityAuthorizationManager.hasAuthority(authority) // (1)      )    );}
public static <T> AuthorityAuthorizationManager<T> hasAuthority(String authority) {    Assert.notNull(authority, "authority cannot be null");    return new AuthorityAuthorizationManager<>(authority);}
public AuthorizationManagerRequestMatcherRegistry access(        AuthorizationManager<RequestAuthorizationContext> manager) {    Assert.notNull(manager, "manager cannot be null");    return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);}
private AuthorityAuthorizationManager<RequestAuthorizationContext> withRoleHierarchy(           AuthorityAuthorizationManager<RequestAuthorizationContext> manager) {    manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get());    return manager;}
  • AuthorityAuthorizationManager.hasAuthority方法简单地创建了一个要求特定authority权限的AuthorityAuthorizationManager实例。

  • withRoleHierarchy是一个装饰器方法,它打开了角色继承的功能。角色继承允许一个角色继承另一个角色的所有权限,从而简化权限配置。

  • 最后,access方法将这个AuthorityAuthorizationManager实例注册到权限控制中。

access方法是公开的,你可以自己实现一个AuthorizationManager,然后通过这个方法进行注册。例如,我们可以提供一个拒绝所有请求的实现:

 
http.authorizeHttpRequests((requests) ->    requests.anyRequest().access((authentication, object) -> null));

【Tips】通过自定义AuthorizationManager,我们可以完全接管鉴权的逻辑,实现更加灵活和复杂的权限控制。

基本架构

相比认证模块,鉴权模块不需要太多的灵活性和扩展性需求,因此它的架构相对简单。

同样,我们以一个标准的鉴权流程为例,来看整体的架构和流程图。

图片

  • 一个HTTP请求进来,经过了一系列Security Filter后,最终来到AuthorizationFilter,进而调用AuthorizationManager#check方法进行权限校验。

  • 实际的校验工作继续委托给AuthoritiesAuthorizationManager

  • AuthoritiesAuthorizationManager先从Security Context中获取到Authentication对象(这个对象一般是前面的某个认证Filter设置的),然后基于其Authorites权限列表构建GrantedAuthority列表,用于权限项的匹配。

  • 最终会返回一个AuthorizationDecision表示权限校验结果。

总结

本文重点分析了Spring Security的源码和架构,帮助读者理解其实现原理。由于篇幅有限,本文只覆盖了身份认证和鉴权模块的核心逻辑,很多特性没有涉及,包括Session管理,Remember Me服务,异常分支和错误处理等等,不过有了上述的基础知识,读者完全可以自己分析源码并深入理解这些特性。

FAQ

认证和鉴权失败抛出的异常是如何处理的?

当发生认证或鉴权失败时,Spring Security有专门的Security Filter ExceptionTranslationFilter来捕获并处理这些异常。如果是认证异常错误AuthenticationException及其子类,会触发AuthenticationEntryPoint#commence方法,而如果是鉴权错误AccessDeniedException及其子类,则会触发AccessDeniedHandler#handle方法。

一个请求被Security拒绝了,应该如何Debug排查?

如果遇到身份认证错误,建议直接Debug相关Filter的doFilter方法,比如Form表单登录的Filter就是UsernamePasswordAuthenticationFilter;而如果是鉴权错误,可以从AuthorizationFilter开始Debug。

但需要注意的是,出于安全考虑,Security相关的错误通常不会提供明确的错误信息,甚至不会显示错误信息,而是直接跳转到登录页面,比如CsrfFilter可能会导致这种情况。在这种情况下,可以从第一个Filter开始Debug,启动日志搜索Will secure any request with,就可以找到所有Security Filter列表。或者直接从入口FilterChainProxy#doFilter开始Debug。

SecurityFilterChain的配置方法底层是如何实现的?

SecurityFilterChain是通过HttpSecurity提供的一套DSL进行配置的。诸如formLogincsrfauthorizeHttpRequests等方法的逻辑都类似,参数都是一个lambda表达式,用于做各种自定义配置。而每个方法都会对应一个特定的配置类,比如FormLoginConfigurerCsrfConfigurer等,在执行HttpSecurity#build方法的时候,会调用这些配置类的configure方法,该方法的作用就是根据用户的自定义配置,创建一个或者多个Security Filter,并将其注册到SecurityFilterChain

此外,开发者还可以通过HttpSecurity.addFilter方法直接添加自定义的Security Filter。而对于复杂且有许多配置选项的Filter,也可以自定义SecurityConfigurerAdapter类,并通过HttpSecurity#apply方法来配置和注册Filter。

Spring Security Starter有哪些默认配置?

Spring Security Starter默认配置在spring-boot-autoconfigure-x.x.x包下的文件META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports中可以找到。而具体的核心配置类有HttpSecurityConfigurationSpringBootWebSecurityConfiguration

Spring Security版本跟本文的不一样,遇到问题如何排查?

每次Spring Security升级,尤其是大版本升级,都可能引入破坏性或者不兼容的更新。不过,基于Filter和SecuiryFilterChain的框架和架构通常是不会改变的。但是,通常会废弃掉老的配置方法,引入新的配置,某些特定模块的实现也有可能完全替换,比如6.0的鉴权模块AuthorizationFilter就完全替换了老的鉴权模块。

你可以先从Security Filter列表开始排查,也可以通过入口FilterChainProxy#doFilter来Debug。

Spring Security整体太复杂了,能不能不使用它,而完全自己实现?

Security是个一个非常复杂的领域,很多开发者对其了解不深。使用Spring Security不仅提供了大部分的安全特性,还包含了很多安全领域的最佳实践。自己从头实现安全功能成本很高,并可能缺乏一些重要的安全特性。不过Spring Security的复杂设计以及频繁的破坏性更新,的确给开发带来了很大的学习成本和维护成本。

Spring Security的架构非常灵活,因此作者的建议是,不需要完全照搬整体框架,对于不同的应用类型和场景,可以选择性地引入部分功能。比如Admin应用可以提供自定义的AuthenticationProvider,而API服务完全可以自定义Securiy Filter,只要维护好Security Context的Authentication,就可以很好的集成到Spring Security框架里,同时开发的学习和维护成本也能降到最低。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值