关于java:Spring-Security没有看起来那么复杂附源码

6次阅读

共计 8812 个字符,预计需要花费 23 分钟才能阅读完成。

权限治理是每个我的项目必备的性能,只是各自要求的复杂程度不同,简略的我的项目可能一个 Filter 或 Interceptor 就解决了,简单一点的就可能会引入平安框架,如 Shiro,Spring Security 等。
其中 Spring Security 因其波及的流程、类过多,看起来比较复杂难懂而被诟病。但如果能捋清其中的关键环节、要害类,Spring Security 其实也没有传说中那么简单。本文联合脚手架框架的权限治理实现(jboost-auth 模块,源码获取见文末),对 Spring Security 的认证、受权机制进行深入分析。

应用 Spring Security 认证、鉴权机制

Spring Security 次要实现了 Authentication(认证——你是谁?)、Authorization(鉴权——你能干什么?)

认证(登录)流程

Spring Security 的认证流程及波及的次要类如下图,

认证入口为 AbstractAuthenticationProcessingFilter,个别实现有 UsernamePasswordAuthenticationFilter

  1. filter 解析申请参数,将客户端提交的用户名、明码等封装为 Authentication,Authentication 个别实现有 UsernamePasswordAuthenticationToken
  2. filter 调用 AuthenticationManager 的 authenticate() 办法对 Authentication 进行认证,AuthenticationManager 的默认实现是
    ProviderManager
  3. ProviderManager 认证时,委托给一个 AuthenticationProvider 列表,调用列表中 AuthenticationProvider 的 authenticate()
    办法来进行认证,只有有一个通过,则认证胜利,否则抛出 AuthenticationException 异样(AuthenticationProvider 还有一个 supports() 办法,用来判断该 Provider
    是否对以后类型的 Authentication 进行认证)
  4. 认证实现后,filter 通过 AuthenticationSuccessHandler(胜利时)或 AuthenticationFailureHandler(失败时)来对认证后果进行解决,如返回 token 或 认证谬误提醒

认证波及的要害类

  1. 登录认证入口 UsernamePasswordAuthenticationFilter

我的项目中 RestAuthenticationFilter 继承了 UsernamePasswordAuthenticationFilter,UsernamePasswordAuthenticationFilter 将客户端提交的参数封装为
UsernamePasswordAuthenticationToken,供 AuthenticationManager 进行认证。

RestAuthenticationFilter 覆写了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(request,response) 办法逻辑,依据
loginType 的值来将登录参数封装到认证信息 Authentication 中,(loginType 为 USER 时为 UsernameAuthenticationToken,
loginType 为 Phone 时为 PhoneAuthenticationToken),供上游 AuthenticationManager 进行认证。

  1. 认证信息 Authentication

应用 Authentication 的实现来保留认证信息,个别为 UsernamePasswordAuthenticationToken,包含

  • principal:身份主体,通常是用户名或手机号
  • credentials:身份凭证,通常是明码或手机验证码
  • authorities:受权信息,通常是角色 Role
  • isAuthenticated:认证状态,示意是否已认证

本我的项目中的 Authentication 实现:

  • UsernameAuthenticationToken:应用用户名登录时封装的 Authentication

    • principal => username
    • credentials => password
    • 扩大了两个属性:uuid,code,用来验证图形验证码
  • PhoneAuthenticationToken:应用手机验证码登录时封装的 Authentication

    • principal => phone(手机号)
    • credentials => code(验证码)

两者都继承了 UsernamePasswordAuthenticationToken。

  1. 认证管理器 AuthenticationManager

认证管理器接口 AuthenticationManager,蕴含一个 authenticate(authentication) 办法。
ProviderManager 是 AuthenticationManager 的实现,治理一个 AuthenticationProvider(具体认证逻辑提供者)列表。在其 `authenticate(authentication
) 办法中,对 AuthenticationProvider 列表中每一个 AuthenticationProvider,调用其 supports(Class<?> authentication)` 办法来判断是否采纳该
Provider 来对 Authentication 进行认证,如果实用则调用 AuthenticationProvider 的 authenticate(authentication)
来实现认证,只有其中一个实现认证,则返回。

  1. 认证提供者 AuthenticationProvider

由 3 可知认证的真正逻辑由 AuthenticationProvider 提供,本我的项目的认证逻辑提供者包含

  • UsernameAuthenticationProvider:反对对 UsernameAuthenticationToken 类型的认证信息进行认证。同时应用 PasswordRetryUserDetailsChecker
    来对明码谬误次数超过 5 次的用户,在 10 分钟内限度其登录操作
  • PhoneAuthenticationProvider:反对对 PhoneAuthenticationToken 类型的认证信息进行认证

两者都继承了 DaoAuthenticationProvider —— 通过 UserDetailsService 的 loadUserByUsername(String username) 获取保留的用户信息
UserDetails,再与客户端提交的认证信息 Authentication 进行比拟(如与 UsernameAuthenticationToken 的明码进行比对),来实现认证。

  1. 用户信息获取 UserDetailsService

UserDetailsService 提供 loadUserByUsername(username) 办法,可获取已保留的用户信息(如保留在数据库中的用户账号信息)。

本我的项目的 UserDetailsService 实现包含

  • UsernameUserDetailsService:通过用户名从数据库获取账号信息
  • PhoneUserDetailsService:通过手机号码从数据库获取账号信息
  1. 认证后果解决

认证胜利,调用 AuthenticationSuccessHandler 的 `onAuthenticationSuccess(request,
response, authentication)` 办法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 时进行了设置。本我的项目中认证胜利后,生成 jwt token 返回客户端。

认证失败(账号校验失败或过程中抛出异样),调用 AuthenticationFailureHandler 的 `onAuthenticationFailure(request,
response, exception)` 办法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 时进行了设置,返回错误信息。

以上要害类及其关联根本都在 SecurityConfiguration 进行配置。

  1. 工具类

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 配置失效。

鉴权流程及波及的次要类如下图,

  1. 登录实现后,个别返回 token 供下次调用时携带进行身份认证,生成 Authentication
  2. FilterSecurityInterceptor 拦截器通过 FilterInvocationSecurityMetadataSource 获取拜访以后资源须要的权限
  3. FilterSecurityInterceptor 调用鉴权管理器 AccessDecisionManager 的 decide 办法进行鉴权
  4. AccessDecisionManager 通过 AccessDecisionVoter 列表的鉴权投票,确定是否通过鉴权,如果不通过则抛出 AccessDeniedException 异样
  5. MethodSecurityInterceptor 流程与 FilterSecurityInterceptor 相似

鉴权波及的要害类

  1. 认证信息提取 RestAuthorizationFilter

对于前后端拆散我的项目,登录实现后,接下来咱们个别通过登录时返回的 token 来拜访接口。

在鉴权开始前,咱们须要将 token 进行验证,而后生成认证信息 Authentication 交给上游进行鉴权(受权)。

本我的项目 RestAuthorizationFilter 将客户端上报的 jwt token 进行解析,失去 UserDetails,并对 token 进行有效性校验,并生成
Authentication(UsernamePasswordAuthenticationToken),通过
SecurityContextHolder 存入 SecurityContext 中供上游应用。

  1. 鉴权入口 AbstractSecurityInterceptor

三个实现:

  • FilterSecurityInterceptor:基于 Filter 的鉴权实现,作用于 Http 接口层级。FilterSecurityInterceptor 从 SecurityMetadataSource 的实现 DefaultFilterInvocationSecurityMetadataSource 获取要拜访资源所须要的权限
    Collection<ConfigAttribute>,而后调用 AccessDecisionManager 进行受权决策投票,若投票通过,则容许拜访资源,否则将禁止拜访。
  • MethodSecurityInterceptor:基于 AOP 的鉴权实现,作用于办法层级。
  • AspectJMethodSecurityInterceptor:用来反对 AspectJ JointPoint 的 MethodSecurityInterceptor
  1. 获取资源权限信息 SecurityMetadataSource

SecurityMetadataSource 读取拜访资源所需的权限信息,读取的内容,就是咱们配置的拜访规定,如咱们在配置类中配置的拜访规定:

@Override
protected void configure(HttpSecurity http) throws Exception{http.authorizeRequests()
        .antMatchers(excludes).anonymous()
        .antMatchers("/api1").hasAuthority("permission1")
        .antMatchers("/api2").hasAuthority("permission2")
        ...
}

咱们能够自定义一个 SecurityMetadataSource 来从数据库或其它存储中获取资源权限规定信息。

  1. 鉴权管理器 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
  1. 鉴权投票者 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_FULLYIS_AUTHENTICATED_REMEMBEREDIS_AUTHENTICATED_ANONYMOUSLY 其中一个,则参加鉴权投票
  1. 鉴权后果解决

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”获取源码地址。



[转载请注明出处]
作者:雨歌,能够关注下作者公众号:半路雨歌

正文完
 0