权限管理是一个几乎所有后台系统的都会涉及的一个重要组成部分,主要目的是对整个后台管理系统进行权限的控制。常见的基于角色的访问控制,其授权模型为“ 用户-角色-权限 ”,简明的说,一个用户拥有多个角色,一个角色拥有多个权限。其中,
实质是:
权限(Permission) = 资源(Resource) + 操作(Privilege)
角色(Role) = 权限的集合(a set of low-level permissions)
用户(User) = 角色的集合(high-level roles)
在实际项目中用户数量多,逐一的为每个系统用户授权,这是极其繁琐的事,所以可以学习linux文件管理系统一样,设置group模式,一组有多个用户,可以为用户组授权相同的权限,简便多了。这样模式下:
每个用户的所有权限=用户个人的权限+用户组所用的权限
用户组、用户、与角色三者关系如下:
再结合权限管理的页面权限、操作权限,如菜单的访问、功能模块的操作、按钮的操作等等,可把功能操作与资源统一管理,即让它们直接与权限关联起来,关系图如下:
FilterSecurityInterceptor doFilter()->invoke() ->AbstractSecurityInterceptor beforeInvocation() ->SecurityMetadataSource 获取ConfigAttribute属性信息(从数据库或者其他数据源地方) getAttributes() ->AccessDecisionManager() 基于AccessDecisionVoter实现授权访问 Decide() ->AccessDecisionVoter 受AccessDecisionManager委托实现授权访问 vote() 复制代码
默认授权过程会使用这样的工作流程,接下来来分析各个组件的功能与源码。
FilterSecurityInterceptor为授权拦截器, 在 FilterSecurityInterceptor 中有一个封装了 过滤链 、 request 以及 response 的 FilterInvocation 对象进行操作,在 FilterSecurityInterceptor ,主要由 invoke() 调用其父类 AbstractSecurityInterceptor 的方法。
invoke()分析:
public void invoke(FilterInvocation fi) throws IOException, ServletException { ..... // 获取accessDecisionManager权限决策后结果状态、以及权限属性 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } } 复制代码
AbstractSecurityInterceptor的授权过滤器主要方法 beforeInvocation() , afterInvocation() 以及 authenticateIfRequired() ,其最主要的方法 beforeInvocation() 分析如下:
protected InterceptorStatusToken beforeInvocation(Object object) { .... //从SecurityMetadataSource的权限属性 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { ..... publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } //调用认证环节获取authenticated(包含用户的详细信息) Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //进行关键的一步:授权的最终决策 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } } 复制代码
SecurityMetadataSource是从数据库或者其他数据源中加载 ConfigAttribute ,为了在 AccessDecisionManager.decide() 最终决策中进行match。其有三个方法:
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;//加载权限资源 Collection<ConfigAttribute> getAllConfigAttributes();//加载所有权限资源 boolean supports(Class<?> var1); 复制代码
AccessDecisionManager被 AbstractSecurityInterceptor 拦截器调用进行最终访问控制决策。 而且由 AuthenticationManager 创建的Authentication object中的GrantedAuthority,首先被授权模块中的 AccessDecisionManager 读取使用,当复杂的GrantedAuthority,getAuthority()为null,因此需要 AccessDecisionManager 专门支持 GrantedAuthority 实现以便了解其内容。
AccessDecisionManager接口方法:
void decide(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attrs) throws AccessDeniedException; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); 复制代码
AccessDecisionManager.decide()将使用 AccessDecisionVoter 进行投票决策。 AccessDecisionVoter 进行投票访问控制决策,访问不通过就抛出 AccessDeniedException 。
** AccessDecisionVoter**接口方法:
int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs); boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); 复制代码
AccessDecisionVoter的 核心方法vote() 通常是获取 Authentication的GrantedAuthority 与 已定义好的ConfigAttributes 进行 match ,如果成功为投同意票,匹配不成功为拒绝票,当ConfigAttributes中无属性时,才投弃票。
Spring Security提供了三种投票方式去实现 AccessDecisionManager 接口进行投票访问控制决策:
ConsensusBased:大多数voter同意访问就授权访问
AffirmativeBased:只要一个以上voter同意访问就授权访问,全部
UnanimousBased :只有全体同意了才授权访问
且 AccessDecisionVoter 用三个静态变量表示voter投票情况:
Note: 当所有voter都弃权时使用变量allowIfEqualGrantedDeniedDecisions来判断,true为通过,false抛出AccessDeniedException。
此外可自定义 AccessDecisionManager 实现接口,因为可能某些AccessDecisionVoter具有权重比高投票权或者某些AccessDecisionVoter具有一票否定权。AccessDecisionVoter的Spring security实现类 RoleVoter 和 AuthenticatedVoter 。 RoleVoter为最为常见的AccessDecisionVoter,其为简单的权限表示,并以前缀为ROLE_,vote匹配规则也跟上面一样。
源码分析:
Public int vote(Authentication authentication,Object object,Collection<ConfigAttribute>attributes){ //用户传递的authentication为null,拒绝访问 if(authentication==null){ return ACCESS_DENIED; } int result=ACCESS_ABSTAIN; Collection<?extendsGrantedAuthority>authorities=extractAuthorities(authentication); //依次进行投票 for(ConfigAttributeattribute:attributes){ if(this.supports(attribute)){ result=ACCESS_DENIED; //Attempt to find a matching granted authority for(GrantedAuthorityauthority:authorities){ if(attribute.getAttribute().equals(authority.getAuthority())){ returnACCESS_GRANTED; } } } } 复制代码
自定义FilterSecurityInterceptor,可仿写 FilterSecurityInterceptor ,实现抽象类 AbstractSecurityInterceptor 以及 Filter 接口,其主要的是把 自定义的SecurityMetadataSource 与 自定义accessDecisionManager 配置到自定义FilterSecurityInterceptor的拦截器中
自定义SecurityMetadataSource,实现接口FilterInvocationSecurityMetadataSource,实现从数据库或者其他数据源中加载ConfigAttribute(即是从数据库或者其他数据源中加载资源权限)
自定义accessDecisionManager,可使用基于AccessDecisionVoter实现权限认证的官方UnanimousBased
自定义MyFilterSecurityInterceptor主要工作为:
加载自定义的SecurityMetadataSource到自定义的FilterSecurityInterceptor中;
加载自定义的AccessDecisionManager到自定义的FilterSecurityInterceptor中;
重写invoke方法
@Component public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private FilterInvocationSecurityMetadataSource securityMetadataSource; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } private void invoke(FilterInvocation fi) throws IOException, ServletException { //fi里面有一个被拦截的url //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限 //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够 InterceptorStatusToken token = super.beforeInvocation(fi); try { //执行下一个拦截器 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } @Override public void destroy() { } @Override public Class<?> getSecureObjectClass() { return null; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } //设置自定义的FilterInvocationSecurityMetadataSource @Autowired public void setSecurityMetadataSource(MyFilterInvocationSecurityMetadataSource messageSource) { this.securityMetadataSource = messageSource; } //设置自定义的AccessDecisionManager @Override @Autowired public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) { super.setAccessDecisionManager(accessDecisionManager); } } 复制代码
自定义MyFilterInvocationSecurityMetadataSource主要工作为:
从数据源中加载ConfigAttribute到SecurityMetadataSource资源器中
重写getAttributes()加载ConfigAttribute为AccessDecisionManager.decide()授权决策做准备。
@Component public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private Map<String, Collection<ConfigAttribute>> configAttubuteMap = null; private void loadResourceDefine() { //todo 加载数据库的所有权限 Collection<ConfigAttribute> attributes; } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { AntPathRequestMatcher matcher; String resUrl; HttpServletRequest request = ((FilterInvocation) object).getRequest(); //1.加载权限资源数据 if (configAttubuteMap == null) { loadResourceDefine(); } Iterator<String> iterator = configAttubuteMap.keySet().iterator(); while (iterator.hasNext()) { resUrl = iterator.next(); matcher = new AntPathRequestMatcher(resUrl); if (matcher.matches(request)) { return configAttubuteMap.get(resUrl); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } } 复制代码
自定义MyAccessDecisionManager主要工作为:
重写最终授权决策decide(),自定义授权访问策略
@Component public class MyAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { ConfigAttribute c; String needRole; if(null== configAttributes || configAttributes.size() <=0) { return; } //1.获取已定义的好资源权限配置 Iterator<ConfigAttribute> iterable=configAttributes.iterator(); while (iterable.hasNext()){ c=iterable.next(); needRole=c.getAttribute(); //2.依次比对用户角色对应的资源权限 for (GrantedAuthority grantedAuthority:authentication.getAuthorities()){ if(needRole.trim().equals(grantedAuthority.getAuthority())){ return; } } } } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } 复制代码
}
配置SecurityConfig主要工作为:
将FilterSecurityInterceptor拦截器加载WebSecurityConfig中
protected void configure(HttpSecurity http) throws Exception { http.headers().frameOptions().disable().and() //表单登录 .formLogin() .loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE) .loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL) .successHandler(authenticationSuccessHandler()) .failureHandler(authenticationFailureHandler()) .and() //应用sms认证配置 .apply(smsAuthenticationSecurityConfig) .and() //允许通过 .authorizeRequests() .antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL, SecurityConstants.APP_USER_REGISTER_URL, SecurityConstants.APP_FORM_LOGIN_INDEX_URL) .permitAll()//以上的请求都不需要认证 .and() //“记住我”配置 .rememberMe() .tokenRepository(jdbcTokenRepository())//token入库处理类 .tokenValiditySeconds(SecurityConstants.REMEMBER_ME_VERIFY_TIME)//remember-me有效时间设置 .rememberMeParameter(SecurityConstants.REMEMBER_ME_PARAM_NAME)//请求参数名设置 .and() .csrf().disable(); //增加自定义权限授权拦截器 http.addFilterBefore(myFilterSecurityInterceptor,FilterSecurityInterceptor.class); } 复制代码
Spring Security授权过程中,可以会涉主要涉及了上面上面所述的组件,其中主要的还是跟着源码多跑几遍,了解其中的原理,才能更加流畅的码代码。到此为止写完Spring Security的认证和授权分析流程,接下来会结合前面小节,写一个Spring security完美的权限管理系统。
最后可关注公众号,一起学习。加群,每天会分享干货,还有学习视频领取!