当前位置 博文首页 > 【雨歌】:Spring Security,没有看起来那么复杂(附源码)
权限管理是每个项目必备的功能,只是各自要求的复杂程度不同,简单的项目可能一个 Filter 或 Interceptor 就解决了,复杂一点的就可能会引入安全框架,如 Shiro, Spring Security 等。
其中 Spring Security 因其涉及的流程、类过多,看起来比较复杂难懂而被诟病。但如果能捋清其中的关键环节、关键类,Spring Security 其实也没有传说中那么复杂。本文结合脚手架框架的权限管理实现(jboost-auth
模块,源码获取见文末),对 Spring Security 的认证、授权机制进行深入分析。
Spring Security 主要实现了 Authentication(认证——你是谁?)、Authorization(鉴权——你能干什么?)
Spring Security 的认证流程及涉及的主要类如下图,
认证入口为 AbstractAuthenticationProcessingFilter,一般实现有 UsernamePasswordAuthenticationFilter
authenticate()
方法对 Authentication 进行认证,AuthenticationManager 的默认实现是authenticate()
supports()
方法,用来判断该 Provider项目中 RestAuthenticationFilter 继承了 UsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter 将客户端提交的参数封装为
UsernamePasswordAuthenticationToken,供 AuthenticationManager 进行认证。
RestAuthenticationFilter 覆写了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(request,response)
方法逻辑,根据
loginType 的值来将登录参数封装到认证信息 Authentication 中,(loginType 为 USER 时为 UsernameAuthenticationToken,
loginType 为 Phone 时为 PhoneAuthenticationToken),供下游 AuthenticationManager 进行认证。
使用 Authentication 的实现来保存认证信息,一般为 UsernamePasswordAuthenticationToken,包括
本项目中的 Authentication 实现:
UsernameAuthenticationToken: 使用用户名登录时封装的 Authentication
PhoneAuthenticationToken: 使用手机验证码登录时封装的 Authentication
两者都继承了 UsernamePasswordAuthenticationToken。
认证管理器接口 AuthenticationManager,包含一个 authenticate(authentication)
方法。
ProviderManager 是 AuthenticationManager 的实现,管理一个 AuthenticationProvider(具体认证逻辑提供者)列表。在其 authenticate(authentication )
方法中,对 AuthenticationProvider 列表中每一个 AuthenticationProvider,调用其 supports(Class<?> authentication)
方法来判断是否采用该
Provider 来对 Authentication 进行认证,如果适用则调用 AuthenticationProvider 的 authenticate(authentication)
来完成认证,只要其中一个完成认证,则返回。
由3可知认证的真正逻辑由 AuthenticationProvider 提供,本项目的认证逻辑提供者包括
两者都继承了 DaoAuthenticationProvider —— 通过 UserDetailsService 的 loadUserByUsername(String username)
获取保存的用户信息
UserDetails,再与客户端提交的认证信息 Authentication 进行比较(如与 UsernameAuthenticationToken 的密码进行比对),来完成认证。
UserDetailsService 提供 loadUserByUsername(username)
方法,可获取已保存的用户信息(如保存在数据库中的用户账号信息)。
本项目的 UserDetailsService 实现包括
认证成功,调用 AuthenticationSuccessHandler 的 onAuthenticationSuccess(request, response, authentication)
方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 时进行了设置。 本项目中认证成功后,生成 jwt token返回客户端。
认证失败(账号校验失败或过程中抛出异常),调用 AuthenticationFailureHandler 的 onAuthenticationFailure(request, response, exception)
方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 时进行了设置,返回错误信息。
以上关键类及其关联基本都在 SecurityConfiguration 进行配置。
SecurityContextHolder 是 SecurityContext 的容器,默认使用 ThreadLocal 存储,使得在相同线程的方法中都可访问到 SecurityContext。
SecurityContext 主要是存储应用的 principal 信息,在 Spring Security 中用 Authentication 来表示。在
AbstractAuthenticationProcessingFilter 中,认证成功后,调用 successfulAuthentication()
方法使用 SecurityContextHolder 来保存
Authentication,并调用 AuthenticationSuccessHandler 来完成后续工作(比如返回token等)。
使用 SecurityContextHolder 来获取用户信息示例:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
Spring Security 的鉴权(授权)有两种实现机制:
@EnableGlobalMethodSecurity(prePostEnabled = true)
使 GlobalMethodSecurityConfiguration 配置生效。鉴权流程及涉及的主要类如下图,
对于前后端分离项目,登录完成后,接下来我们一般通过登录时返回的 token 来访问接口。
在鉴权开始前,我们需要将 token 进行验证,然后生成认证信息 Authentication 交给下游进行鉴权(授权)。
本项目 RestAuthorizationFilter 将客户端上报的 jwt token 进行解析,得到 UserDetails, 并对 token 进行有效性校验,并生成
Authentication(UsernamePasswordAuthenticationToken),通过
SecurityContextHolder 存入 SecurityContext 中供下游使用。
三个实现:
SecurityMetadataSource 读取访问资源所需的权限信息,读取的内容,就是我们配置的访问规则,如我们在配置类中配置的访问规则:
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
.antMatchers(excludes).anonymous()
.antMatchers("/api1").hasAuthority("permission1")
.antMatchers("/api2").hasAuthority("permission2")
...
}
我们可以自定义一个 SecurityMetadataSource 来从数据库或其它存储中获取资源权限规则信息。
AccessDecisionManager 接口的 decide(authentication, object, configAttributes)
方法对本次请求进行鉴权,其中
AccessDecisionManager 接口的实现者鉴权时,最终是通过调用其内部 List<AccessDecisionVoter<?>>
列表中每一个元素的 vote(authentication, object, attributes)
方法来进行的,根据决策的不同分为如下三种实现
AccessDecisionVoter.vote
返回 AccessDecisionVoter.与 AuthenticationProvider 类似,AccessDecisionVoter 也包含 supports(attribute)
方法(是否采用该 Voter 来对请求进行鉴权投票) 与 vote (authentication, object, attributes)
方法(具体的鉴权投票逻辑)
FilterSecurityInterceptor 的 AccessDecisionManager 的投票者列表(AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor() 中设置)包括:
MethodSecurityInterceptor 的 AccessDecisionManager 的投票者列表(GlobalMethodSecurityConfiguration.accessDecisionManager()
中设置)包括:
ConfigAttribute.getAttribute()
以 ROLE_
开头,则参与鉴权投票ConfigAttribute.getAttribute()
值为IS_AUTHENTICATED_FULLY
,IS_AUTHENTICATED_REMEMBERED
,IS_AUTHENTICATED_ANONYMOUSLY
其中一个,则参与鉴权投票ExceptionTranslationFilter 异常处理 Filter, 对认证鉴权过程中抛出的异常进行处理,包括:
如果是 MethodSecurityInterceptor 鉴权时抛出 AccessDeniedException,并且通过 @RestControllerAdvice 提供了统一异常处理,则将由统一异常处理类处理,因为
MethodSecurityInterceptor 是 AOP 机制,可由 @RestControllerAdvice 捕获。
本项目中, RestAuthorizationFilter 在 Filter 链中位于 ExceptionTranslationFilter 的前面,所以其中抛出的异常也不能被 ExceptionTranslationFilter 捕获, 由 cn.jboost.base.starter.web.ExceptionHandlerFilter 捕获处理。
也可以将 RestAuthorizationFilter 放入 ExceptionTranslationFilter 之后,但在 RestAuthorizationFilter 中需要对 SecurityContextHolder.getContext().getAuthentication()
进行 AnonymousAuthenticationToken 的判断,因为 AnonymousAuthenticationFilter 位于 ExceptionTranslationFilter 前面,会对 Authentication 为空的请求生成一个
AnonymousAuthenticationToken,放入 SecurityContext 中。
安全框架一般包括认证与授权两部分,认证解决你是谁的问题,即确定你是否有合法的访问身份,授权解决你是否有权限访问对应资源的问题。Spring Security 使用 Filter 来实现认证,使用 Filter(接口层级) + AOP(方法层级)的方式来实现授权。本文相对偏理论,但也结合了脚手架中的实现,对照查看,应该更易理解。
本文基于 Spring Boot 脚手架中的权限管理模块编写,该脚手架提供了前后端分离的权限管理实现,效果如下图,可关注作者公众号 “半路雨歌”,回复 “jboost” 获取源码地址。
[转载请注明出处]
作者:雨歌,可以关注作者公众号:半路雨歌