权限治理是每个我的项目必备的性能,只是各自要求的复杂程度不同,简略的我的项目可能一个 Filter 或 Interceptor 就解决了,简单一点的就可能会引入平安框架,如 Shiro,Spring Security 等。
其中 Spring Security 因其波及的流程、类过多,看起来比较复杂难懂而被诟病。但如果能捋清其中的关键环节、要害类,Spring Security 其实也没有传说中那么简单。本文联合脚手架框架的权限治理实现(jboost-auth
模块,源码获取见文末),对 Spring Security 的认证、受权机制进行深入分析。
应用 Spring Security 认证、鉴权机制
Spring Security 次要实现了 Authentication(认证——你是谁?)、Authorization(鉴权——你能干什么?)
认证(登录)流程
Spring Security 的认证流程及波及的次要类如下图,
认证入口为 AbstractAuthenticationProcessingFilter,个别实现有 UsernamePasswordAuthenticationFilter
- filter 解析申请参数,将客户端提交的用户名、明码等封装为 Authentication,Authentication 个别实现有 UsernamePasswordAuthenticationToken
- filter 调用 AuthenticationManager 的
authenticate()
办法对 Authentication 进行认证,AuthenticationManager 的默认实现是
ProviderManager - ProviderManager 认证时,委托给一个 AuthenticationProvider 列表,调用列表中 AuthenticationProvider 的
authenticate()
办法来进行认证,只有有一个通过,则认证胜利,否则抛出 AuthenticationException 异样(AuthenticationProvider 还有一个supports()
办法,用来判断该 Provider
是否对以后类型的 Authentication 进行认证) - 认证实现后,filter 通过 AuthenticationSuccessHandler(胜利时)或 AuthenticationFailureHandler(失败时)来对认证后果进行解决,如返回 token 或 认证谬误提醒
认证波及的要害类
- 登录认证入口 UsernamePasswordAuthenticationFilter
我的项目中 RestAuthenticationFilter 继承了 UsernamePasswordAuthenticationFilter,UsernamePasswordAuthenticationFilter 将客户端提交的参数封装为
UsernamePasswordAuthenticationToken,供 AuthenticationManager 进行认证。
RestAuthenticationFilter 覆写了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(request,response)
办法逻辑,依据
loginType 的值来将登录参数封装到认证信息 Authentication 中,(loginType 为 USER 时为 UsernameAuthenticationToken,
loginType 为 Phone 时为 PhoneAuthenticationToken),供上游 AuthenticationManager 进行认证。
- 认证信息 Authentication
应用 Authentication 的实现来保留认证信息,个别为 UsernamePasswordAuthenticationToken,包含
- principal:身份主体,通常是用户名或手机号
- credentials:身份凭证,通常是明码或手机验证码
- authorities:受权信息,通常是角色 Role
- isAuthenticated:认证状态,示意是否已认证
本我的项目中的 Authentication 实现:
-
UsernameAuthenticationToken:应用用户名登录时封装的 Authentication
- principal => username
- credentials => password
- 扩大了两个属性:uuid,code,用来验证图形验证码
-
PhoneAuthenticationToken:应用手机验证码登录时封装的 Authentication
- principal => phone(手机号)
- credentials => code(验证码)
两者都继承了 UsernamePasswordAuthenticationToken。
- 认证管理器 AuthenticationManager
认证管理器接口 AuthenticationManager,蕴含一个 authenticate(authentication)
办法。
ProviderManager 是 AuthenticationManager 的实现,治理一个 AuthenticationProvider(具体认证逻辑提供者)列表。在其 `authenticate(authentication
) 办法中,对 AuthenticationProvider 列表中每一个 AuthenticationProvider,调用其
supports(Class<?> authentication)` 办法来判断是否采纳该
Provider 来对 Authentication 进行认证,如果实用则调用 AuthenticationProvider 的 authenticate(authentication)
来实现认证,只有其中一个实现认证,则返回。
- 认证提供者 AuthenticationProvider
由 3 可知认证的真正逻辑由 AuthenticationProvider 提供,本我的项目的认证逻辑提供者包含
- UsernameAuthenticationProvider:反对对 UsernameAuthenticationToken 类型的认证信息进行认证。同时应用 PasswordRetryUserDetailsChecker
来对明码谬误次数超过 5 次的用户,在 10 分钟内限度其登录操作 - PhoneAuthenticationProvider:反对对 PhoneAuthenticationToken 类型的认证信息进行认证
两者都继承了 DaoAuthenticationProvider —— 通过 UserDetailsService 的 loadUserByUsername(String username)
获取保留的用户信息
UserDetails,再与客户端提交的认证信息 Authentication 进行比拟(如与 UsernameAuthenticationToken 的明码进行比对),来实现认证。
- 用户信息获取 UserDetailsService
UserDetailsService 提供 loadUserByUsername(username)
办法,可获取已保留的用户信息(如保留在数据库中的用户账号信息)。
本我的项目的 UserDetailsService 实现包含
- UsernameUserDetailsService:通过用户名从数据库获取账号信息
- PhoneUserDetailsService:通过手机号码从数据库获取账号信息
- 认证后果解决
认证胜利,调用 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 的鉴权(受权)有两种实现机制:
- FilterSecurityInterceptor:通过 Filter 对 HTTP 资源的拜访进行鉴权
- MethodSecurityInterceptor:通过 AOP 对办法的调用进行鉴权。在 GlobalMethodSecurityConfiguration 中注入,
须要在配置类上增加注解 @EnableGlobalMethodSecurity(prePostEnabled = true)
使 GlobalMethodSecurityConfiguration 配置失效。
鉴权流程及波及的次要类如下图,
- 登录实现后,个别返回 token 供下次调用时携带进行身份认证,生成 Authentication
- FilterSecurityInterceptor 拦截器通过 FilterInvocationSecurityMetadataSource 获取拜访以后资源须要的权限
- FilterSecurityInterceptor 调用鉴权管理器 AccessDecisionManager 的 decide 办法进行鉴权
- AccessDecisionManager 通过 AccessDecisionVoter 列表的鉴权投票,确定是否通过鉴权,如果不通过则抛出 AccessDeniedException 异样
- MethodSecurityInterceptor 流程与 FilterSecurityInterceptor 相似
鉴权波及的要害类
- 认证信息提取 RestAuthorizationFilter
对于前后端拆散我的项目,登录实现后,接下来咱们个别通过登录时返回的 token 来拜访接口。
在鉴权开始前,咱们须要将 token 进行验证,而后生成认证信息 Authentication 交给上游进行鉴权(受权)。
本我的项目 RestAuthorizationFilter 将客户端上报的 jwt token 进行解析,失去 UserDetails,并对 token 进行有效性校验,并生成
Authentication(UsernamePasswordAuthenticationToken),通过
SecurityContextHolder 存入 SecurityContext 中供上游应用。
- 鉴权入口 AbstractSecurityInterceptor
三个实现:
- FilterSecurityInterceptor:基于 Filter 的鉴权实现,作用于 Http 接口层级。FilterSecurityInterceptor 从 SecurityMetadataSource 的实现 DefaultFilterInvocationSecurityMetadataSource 获取要拜访资源所须要的权限
Collection<ConfigAttribute>,而后调用 AccessDecisionManager 进行受权决策投票,若投票通过,则容许拜访资源,否则将禁止拜访。 - MethodSecurityInterceptor:基于 AOP 的鉴权实现,作用于办法层级。
- AspectJMethodSecurityInterceptor:用来反对 AspectJ JointPoint 的 MethodSecurityInterceptor
- 获取资源权限信息 SecurityMetadataSource
SecurityMetadataSource 读取拜访资源所需的权限信息,读取的内容,就是咱们配置的拜访规定,如咱们在配置类中配置的拜访规定:
@Override
protected void configure(HttpSecurity http) throws Exception{http.authorizeRequests()
.antMatchers(excludes).anonymous()
.antMatchers("/api1").hasAuthority("permission1")
.antMatchers("/api2").hasAuthority("permission2")
...
}
咱们能够自定义一个 SecurityMetadataSource 来从数据库或其它存储中获取资源权限规定信息。
- 鉴权管理器 AccessDecisionManager
AccessDecisionManager 接口的 decide(authentication, object, configAttributes)
办法对本次申请进行鉴权,其中
- authentication:本次申请的认证信息,蕴含 authority(如角色)信息
- object:以后被调用的被爱护对象,如接口
- configAttributes:与被爱护对象关联的配置属性,示意要拜访被爱护对象须要满足的条件,如角色
AccessDecisionManager 接口的实现者鉴权时,最终是通过调用其外部 List<AccessDecisionVoter<?>>
列表中每一个元素的 vote(authentication, object, attributes)
办法来进行的,依据决策的不同分为如下三种实现
- AffirmativeBased:一票通过权策略。只有有一个 AccessDecisionVoter 通过(
AccessDecisionVoter.vote
返回 AccessDecisionVoter.
ACCESS_GRANTED),则鉴权通过。为默认实现 - ConsensusBased:多数遵从少数策略。少数 AccessDecisionVoter 通过,则鉴权通过,如果赞成票与反对票相等,则依据变量 allowIfEqualGrantedDeniedDecisions
的值来决定,该值默认为 true - UnanimousBased:全票通过策略。所有 AccessDecisionVoter 通过或弃权(返回 AccessDecisionVoter.
ACCESS_ABSTAIN),无一拥护则通过,只有有一个拥护就回绝;如果全副弃权,则依据变量 allowIfAllAbstainDecisions 的值来决定,该值默认为 false
- 鉴权投票者 AccessDecisionVoter
与 AuthenticationProvider 相似,AccessDecisionVoter 也蕴含 supports(attribute)
办法(是否采纳该 Voter 来对申请进行鉴权投票)与 `vote
(authentication, object, attributes)` 办法(具体的鉴权投票逻辑)
FilterSecurityInterceptor 的 AccessDecisionManager 的投票者列表(AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor() 中设置)包含:
- WebExpressionVoter:验证 Authentication 的 authenticated。
MethodSecurityInterceptor 的 AccessDecisionManager 的投票者列表(GlobalMethodSecurityConfiguration.accessDecisionManager()
中设置)包含:
- PreInvocationAuthorizationAdviceVoter:如果 @EnableGlobalMethodSecurity 注解开启了 prePostEnabled,则增加该 Voter,对应用了 @PreAuthorize 注解的办法进行鉴权投票
- Jsr250Voter:如果 @EnableGlobalMethodSecurity 注解开启了 jsr250Enabled,则增加该 Voter,对 @Secured 注解的办法进行鉴权投票
- RoleVoter:总是增加, 如果
ConfigAttribute.getAttribute()
以ROLE_
结尾,则参加鉴权投票 - AuthenticatedVoter:总是增加,如果
ConfigAttribute.getAttribute()
值为IS_AUTHENTICATED_FULLY
,IS_AUTHENTICATED_REMEMBERED
,IS_AUTHENTICATED_ANONYMOUSLY
其中一个,则参加鉴权投票
- 鉴权后果解决
ExceptionTranslationFilter 异样解决 Filter,对认证鉴权过程中抛出的异样进行解决,包含:
- authenticationEntryPoint:对过滤器链中抛出 AuthenticationException 或 AccessDeniedException 但 Authentication 为
AnonymousAuthenticationToken 的状况进行解决。如果 token 校验失败,如 token 谬误或过期,则通过 ExceptionTranslationFilter 的 AuthenticationEntryPoint 进行解决,本我的项目应用 RestAuthenticationEntryPoint 来返回对立格局的错误信息
- accessDeniedHandler:对过滤器链中抛出 AccessDeniedException 但 Authentication 不为 AnonymousAuthenticationToken 的状况进行解决,本我的项目应用 RestAccessDeniedHandler 来返回对立格局的错误信息
如果是 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”获取源码地址。
[转载请注明出处]
作者:雨歌,能够关注下作者公众号:半路雨歌