SpringBoot整合SpringSecurity实现接口动态管理权限
接上一篇权限管理是后台管理不可缺少的部分,今天结合SpringSecurity实现接口的动态管理。
动态权限管理
SpringSecurity实现权限动态管理,第一步需要创建一个过滤器,doFilter方法需要注意,对于OPTIONS直接放行,否则会出现跨域问题。并且对在上篇文章提到的IgnoreUrlsConfig中的白名单也是直接放行,所有的权限操作都会在super.beforeInvocation(fi)中实现。
/** * 动态权限过滤器,用于实现基于路径的动态权限过滤 * */ public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter { @Autowired private DynamicSecurityMetadataSource dynamicSecurityMetadataSource; @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Autowired public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) { super.setAccessDecisionManager(dynamicAccessDecisionManager); } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); //OPTIONS请求直接放行 if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } //白名单请求直接放行 PathMatcher pathMatcher = new AntPathMatcher(); for (String path : ignoreUrlsConfig.getUrls()) { if(pathMatcher.match(path,request.getRequestURI())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } } //此处会调用AccessDecisionManager中的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 FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return dynamicSecurityMetadataSource; } }
在DynamicSecurityFilter中调用super.beforeInvocation(fi)方法时会调用AccessDecisionManager中的decide方法用于鉴权操作,而decide方法中的configAttributes参数会通过SecurityMetadataSource中的getAttributes方法来获取,configAttributes其实就是配置好的访问当前接口所需要的权限,下面是简化版的beforeInvocation源码
public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware { protected InterceptorStatusToken beforeInvocation(Object object) { //获取元数据 Collection<ConfigAttribute> 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接口的getAttributes方法,来获取当前访问的路径资源
/** * 动态权限数据源,用于获取动态权限规则 * */ public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static Map<String, ConfigAttribute> configAttributeMap = null; @Autowired private DynamicSecurityService dynamicSecurityService; @PostConstruct public void loadDataSource() { configAttributeMap = dynamicSecurityService.loadDataSource(); } public void clearDataSource() { configAttributeMap.clear(); configAttributeMap = null; } @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { if (configAttributeMap == null) this.loadDataSource(); List<ConfigAttribute> configAttributes = new ArrayList<>(); //获取当前访问的路径 String url = ((FilterInvocation) o).getRequestUrl(); String path = URLUtil.getPath(url); PathMatcher pathMatcher = new AntPathMatcher(); Iterator<String> iterator = configAttributeMap.keySet().iterator(); //获取访问该路径所需资源 while (iterator.hasNext()) { String pattern = iterator.next(); if (pathMatcher.match(pattern, path)) { configAttributes.add(configAttributeMap.get(pattern)); } } // 未设置操作请求权限,返回空集合 return configAttributes; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
我们的后台资源被规则缓存到了一个MAP对象中,所有当后台资源变化时,需要清除缓存,在下次查询的时候重新加载。我们需要修改MyMesResourceController注入DynamicSecurityMetadataSource,当修改后台资源时,需要调用clearDataSource方法来清空缓存的数据。
/** * 后台资源管理Controller * */ @Controller @Api(tags = "MyMesResourceController", description = "后台资源管理") @RequestMapping("/resource") public class MyMesResourceController { @Autowired private MyMesResourceService resourceService; @Autowired private DynamicSecurityMetadataSource dynamicSecurityMetadataSource; @ApiOperation("添加后台资源") @RequestMapping(value = "/create", method = RequestMethod.POST) @ResponseBody public CommonResult create(@RequestBody UmsResource umsResource) { int count = resourceService.create(umsResource); dynamicSecurityMetadataSource.clearDataSource(); if (count > 0) { return CommonResult.success(count); } else { return CommonResult.failed(); } } }
我们需要实现AccessDecisionManager接口来实现权限校验,对于没有配置资源的接口我们直接允许访问,对于配置了资源的接口,我们把访问所需资源和用户拥有的资源进行比对,如果匹配则允许访问。
/** * 动态权限决策管理器,用于判断用户是否有访问权限 * */ public class DynamicAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { // 当接口未被配置资源时直接放行 if (CollUtil.isEmpty(configAttributes)) { return; } Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while (iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //将访问所需资源或用户拥有资源进行比对 String needAuthority = configAttribute.getAttribute(); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { if (needAuthority.trim().equals(grantedAuthority.getAuthority())) { return; } } } throw new AccessDeniedException("抱歉,您没有访问权限"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
我们之前在DynamicSecurityMetadataSource中注入了一个DynamicSecurityService对象,它是我自定义的一个动态权限业务接口,其主要用于加载所有的后台资源规则。
/** * 动态权限相关业务类 * */ public interface DynamicSecurityService { /** * 加载资源ANT通配符和资源对应MAP */ Map<String, ConfigAttribute> loadDataSource(); }
结合SpringSecurity实现接口的动态管理权限基本已经实现,明天后天准备讲解一下Redis+AOP优化权限管理