-
UserDetailsService
:根据用户名查找用户信息的组件,默认配置的是内存存储InMemoryUserDetailsManager
,你也可以配置为内置的数据库存储JdbcUserDetailsManager
,但是它有很多默认的约定要遵守,对未来的扩展也不够灵活。通常会根据公司的规范要求或数据库存储的方式提供自定义的实现。 -
PasswordEncoder
:对密码进行编码的组件,建议根据公司的编码要求或当前数据库中已使用的编码来配置。如果没有特殊要求,建议采用默认的BCryptPasswordEncoder
。 -
AuthenticationProvider
: 为了安全需要,公司内部很多应用是不允许直接访问用户的密码的,而通常会提供一个认证的API。此时,就需要自定义AuthenticationProvider
,它的核心逻辑就是调用API做认证,然后把结果再包装成Authentication
返回给AuthenticationManager
。 -
AuthenticationManager
:它的默认实现ProviderManager
适用于大部分场景,通常不需要替换,除非你不想引入太多的概念。 -
UsernamePasswordAuthenticationFilter
:如果你不想引入过多的概念和复杂度,可以提供自己的Security Filter,从而完全脱离该框架。但是需要确保认证结果模型Authentication
仍然被正确处理,并且将结果通过方法SecurityContextHolder.getContext().setAuthentication
放入Security Context中。
【Tips】从整个Security框架的角度来看,认证模块的核心概念只有两个,分别是认证结果
Authentication
和Security Context
。其它概念都可以认为是认证模块的内部实现细节。
鉴权模块Authorization
认证模块证明了用户的身份,但显然普通用户不应该可以随意访问管理页面或敏感资源,因此还需要有个模块来确保只有授权的用户才能执行特定的操作,这个模块称之为鉴权或者授权(Authorization)。
当你通过HttpSecurity.authorizeHttpRequests
方法来配置请求的访问权限控制时,就会自动添加鉴权的Security Filter:AuthorizationFilter
,它是整个SecurityFilterChain
的最后一个Filter。
【版本兼容性】在Spring Security 6.0版本中,鉴权模块发生了很大变化。以前的版本中,鉴权模块使用
FilterSecurityInterceptor
,而6.0版本之后,这个被废弃了,取而代之的是AuthorizationFilter
。同时,还有一些相关的依赖组件,如AccessDecisionManager
和AccessDecisionVoter
也被AuthorizationManger
替换了。因此,本节的内容只限于6.0以及之后的版本。
实现原理
我们先看下鉴权模块的入口,也就是AuthorizationFilter
的doFilter
方法:
@Override
public 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;
}
-
这个方法本身很简单,核心逻辑都委托给了
AuthorizationManager
。AuthorizationManager
会校验Authentication
的权限,并返回鉴权的结果AuthorizationDecision
。 -
如果当前认证用户没有访问权限,就会抛出
AccessDeniedException
异常,表示拒绝访问。 -
待校验的
Authentication
是从Security Context获取的,通常是在前面的认证阶段设置的。在这里,实际上传给AuthorizationManager
的是一个获取Authentication
的方法,而不是Authentication
本身,这样就把实际的获取操作延后到了真正进行授权的时候,这在某些场景下可以提高性能,比如permitAll
,实际上它根本用不到Authentication
。
AuthorizationManager
AuthorizationManager
才是真正执行鉴权逻辑的类,最常用的实现类是AuthorityAuthorizationManager
,它的实现逻辑很简单,它会调用Authentication
的getAuthorities
方法,获取当前登录用户的权限列表,然后将这些权限与请求需要的权限进行匹配。
实际上,选择使用哪个AuthorizationManager
是开发手动设置的。我们来分析一个常用的权限配置代码片段:
@Bean
static 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
方法就相当于打开了鉴权模块,它会注册AuthorizationFilter
到SecurityFilterChain
的最后。 -
对于匹配
/admin
的请求,要求有ROLE_ADMIN
权限。hasAuthority
的底层就是配置了一个要求ROLE_ADMIN
权限的AuthorityAuthorizationManager
对象。 -
对于匹配
/hello
的请求,要求有USER
角色,等价于ROLE_USER
权限。hasRole
会自动在角色名称前面加上前缀ROLE_
。hasRole
的底层就是配置了一个要求ROLE_USER
权限的AuthorityAuthorizationManager
对象。 -
对于其它的请求,只要通过身份认证就可以访问,不需要特定的权限。类似的,
authenticated
方法的底层配置了一个AuthenticatedAuthorizationManager
对象。
在Spring Security中,很多初学者都容易混淆Role
和Authority
的区别,实际上在技术实现层面上,这两者没有本质区别,底层都仅仅是一个表示权限的字符串标识符。更多的区别在于权限管理的概念上,一般情况下,Authority
表示细粒度的操作权限,比如ADD_USER
,DELETE_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进行配置的。诸如formLogin
,csrf
,authorizeHttpRequests
等方法的逻辑都类似,参数都是一个lambda表达式,用于做各种自定义配置。而每个方法都会对应一个特定的配置类,比如FormLoginConfigurer
,CsrfConfigurer
等,在执行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
中可以找到。而具体的核心配置类有HttpSecurityConfiguration
和SpringBootWebSecurityConfiguration
。
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框架里,同时开发的学习和维护成本也能降到最低。