#|玩转Spring Cloud Security OAuth2资源授权动态权限扩展

在Spring Cloud Security 中,认证和授权都是通过FilterChainProxy(Servlet Filter过滤器)拦截然后进行操作的,详细的操作流程可以参考深入理解Spring Cloud Security OAuth2资源授权。FilterSecurityInterceptor过滤器拦截了受保护资源的请求,然后进行授权处理,授权验证的逻辑在其父类AbstractSecurityInterceptor实现。大致流程如下:

  1. 使用SecurityMetadataSource根据http请求获取对应拥有的权限。
  2. 使用Spring Security授权模块对用户访问的资源进行授权验证。
【#|玩转Spring Cloud Security OAuth2资源授权动态权限扩展】AbstractSecurityInterceptor的部分源码如下:
// AbstractSecurityInterceptor.java protected InterceptorStatusToken beforeInvocation(Object object) { ......// 根据http请求获取对应的配置的权限信息 Collection attributes = this.obtainSecurityMetadataSource() .getAttributes(object); ...... // 对用户认证进行校验 Authentication authenticated = authenticateIfRequired(); try { // 对用户的权限与访问资源拥有的权限进行校验 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } ...... }

在默认的SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource实现中,会把资源服务器配置的权限信息全部加载到内存。如果要实现授权权限的动态修改,需要扩展SecurityMetadataSource,例如,使权限数据能够动态的从数据库获取。并且,自定义根据动态权限认证逻辑AccessDecisionVoter。
扩展SecurityMetadataSource
自定义PermissionFilterInvocationSecurityMetadataSource,参考默认的DefaultFilterInvocationSecurityMetadataSource实现从数据库动态的根据访问http请求获取配置的权限。由于每次都需要获取全部的有效的权限配置数据,可以对权限数据做一个本地缓存,提交查询效率。在ConfigAttribute的子类实现中,可以使用SecurityConfig保存配置的权限,ConfigAttribute的子类实现以及使用方式可以参考Spring Security教程 Vol 7. 访问规则ConfigAttribute。实现代码如下:
public class PermissionFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {private final PermissionClient permissionClient; public PermissionFilterInvocationSecurityMetadataSource(PermissionClient permissionClient) { this.permissionClient = permissionClient; }/** * 转换权限列表 */ private Map> requestMatcherCollectionMap() {List allPermissions = permissionClient.findAllList(); if (CollectionUtils.isEmpty(allPermissions)) { return ImmutableMap.of(); } return allPermissions.stream() .collect(Collectors.toMap(permission -> new AntPathRequestMatcher(permission.getUrl()), permission -> Lists.newArrayList(new SecurityConfig(permission.getCode())))); }@Override public Collection getAttributes(Object object) throws IllegalArgumentException {final HttpServletRequest request = ((FilterInvocation) object).getRequest(); for (Map.Entry> entry : requestMatcherCollectionMap().entrySet()) { if (entry.getKey().matches(request)) { return entry.getValue(); } } return null; }@Override public Collection getAllConfigAttributes() { return requestMatcherCollectionMap().values() .stream().flatMap(Collection::stream) .collect(Collectors.toList()); }@Override public boolean supports(Class clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }

扩展根据权限授权逻辑
自定义PermissionsVoter类实现AccessDecisionVoter接口,实现了用户只要拥有访问资源的权限就可以访问。参考RoleVoter具体的实现逻辑,代码如下:
public class PermissionsVoter implements AccessDecisionVoter {@Override public boolean supports(ConfigAttribute attribute) { return Objects.nonNull(attribute.getAttribute()); }@Override public boolean supports(Class clazz) { return true; }@Override public int vote(Authentication authentication, Object object, Collection attributes) {if (CollectionUtils.isEmpty(attributes)) { return ACCESS_DENIED; } // 用户授权的权限 Collection grantedAuthorities; if (Objects.isNull(authentication) || CollectionUtils.isEmpty(grantedAuthorities = extractAuthorities(authentication)) || Objects.isNull(object)) {log.info("user no authentication!"); return ACCESS_DENIED; } for (GrantedAuthority grantedAuthority : grantedAuthorities) {String authority; if (StringUtils.isNotBlank(authority = grantedAuthority.getAuthority()) && match(authority, attributes)) { return ACCESS_GRANTED; } } return ACCESS_DENIED; }private boolean match(String authority, Collection attributes) {for (ConfigAttribute configAttribute : attributes) { String attribute; if (StringUtils.isNotBlank(attribute = configAttribute.getAttribute()) && attribute.equals(authority)) { return true; } } return false; }/** * 获取用户权限列表 */ Collection extractAuthorities(Authentication authentication) { return authentication.getAuthorities(); } }
配置资源服务器
在配置资源服务器,主要是如下配置:
  • SecurityMetadataSource,获取资源权限的设置
  • AccessDecisionManager,自定义授权逻辑的配置
重点讲解下自定义AccessDecisionManager的情况,
  1. 选择AffirmativeBased(只要有一个授权处理通过则可以进行访问)。
  2. 配置RoleVoter(角色授权),AuthenticatedVoter(认证信息授权), WebExpressionVoter(EL描述授权)spring security默认的授权逻辑。
  3. 重点讲解WebExpressionVoter的初始化。在生成WebExpressionVoter时,需要设置其expressionHandler为OAuth2WebSecurityExpressionHandler,这样在进行验证时才不会报错。在使用默认的AccessDecisionManager启动进行验证时,Spring Security使用ExpressionUrlAuthorizationConfigurer默认配置WebExpressionVoter,并且在设置expressionHandler为OAuth2WebSecurityExpressionHandler。使用默认配置资源服务器启动时,调试的结果如下:
#|玩转Spring Cloud Security OAuth2资源授权动态权限扩展
文章图片

在资源资源服务器中的详细配置如下:
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter {/** * 资源服务器内的资源访问控制 */ @Override public void configure(HttpSecurity http) throws Exception {http.authorizeRequests() .antMatchers("/webjars/**", "/v2/api-docs", "/swagger-resources/**", "/swagger-ui.html", "/swagger.json").permitAll() .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor() {@Override public O postProcess(O fsi) { // 权限获取自定义配置 fsi.setSecurityMetadataSource(new PermissionFilterInvocationSecurityMetadataSource(permissionClient)); return fsi; } }) .accessDecisionManager(accessDecisionManager()); }private AccessDecisionManager accessDecisionManager() {WebExpressionVoter webExpressionVoter = new WebExpressionVoter(); webExpressionVoter.setExpressionHandler(new OAuth2WebSecurityExpressionHandler()); // 授权逻辑自定义配置 return new AffirmativeBased(Lists.newArrayList(new PermissionsVoter(), new RoleVoter(), new AuthenticatedVoter(), webExpressionVoter)); } }


授权测试
在db中配置用户username为admin,password为123456的用户拥有couponDemo的访问权限,在使用postman先认证,然后携带访问coupon/demo api,结果正常返回,操作如图:
#|玩转Spring Cloud Security OAuth2资源授权动态权限扩展
文章图片

在访问时,调用自定义PermissionFilterInvocationSecurityMetadataSource获取配置权限的截图如下:
#|玩转Spring Cloud Security OAuth2资源授权动态权限扩展
文章图片

在进行授权处理时,调用自定义 PermissionsVoter进行授权认证,截图如下:
#|玩转Spring Cloud Security OAuth2资源授权动态权限扩展
文章图片

不足与优化之处
一般的实现中,在每个单独的微服务中配置资源服务器,资源授权成功以后,SecurityContextPersistenceFilter已经把当前登录用户信息存储到SecurityContextHolder上下文,直接根据security提供的SecurityContextHolder.getContext().getAuthentication().getPrincipal()就可以获取当前登录用户信息。如果,微服务不断地增加,一般常见的电商系统都有用户服务,商品服务,订单服务等等,这时,该如何配置资源服务器呢?请关注后续的微服务网关章节。

    推荐阅读