https://www.jb51.net/article/141682.htm
https://blog.csdn.net/weixin_43184769/article/details/84937685#t0
动态加载URL权限
动态实际测试项目:https://gitee.com/sw008/springbootdemo_source_code
项目目的是实现Spring Security从DB中加载URL的相关权限。且当DB中配置发生更改时,可以让运行中的项目无需重启,动态更改权限缓存。
整体思路:自定义资源管理器加载并管理URL权限,自定义决策器从资源管理器获得请求对应权限与用户Authentication进行匹配。将自定义的资源管理器和决策器通过AbstractSecurityInterceptor注入到Security框架环境中。并对外暴露资源管理器加载map缓存的接口,提供动态刷新功能。
Spring Security中拦截鉴权最重要的是org.springframework.security.web.access.intercept.FilterSecurityInterceptor,该过滤器实现了主要的鉴权逻辑,最核心的代码在这里:
class FilterSecurityInterceptor
protected InterceptorStatusToken beforeInvocation(Object object) {
//对应方法1。通过FilterInvocationSecurityMetadataSource实现类,获取URL所对应的权限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
Authentication authenticated = authenticateIfRequired();
//对应方法2。通过AccessDecisionManager实现类鉴权
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// 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);
}
}
从上面可以看出,要实现URL权限动态加载,可以从两方面着手:
资源授权器:自定义SecurityMetadataSource(URL权限源) 其需要实现FilterInvocationSecurityMetadataSource接口。
功能1:是从BD加载URL以及对应权限,保存到HashMap<String, List<ConfigAttribute>> map中。key:url,value:所需权限List<ConfigAttribute>。
功能2:实现getAttributes方法,通过功能1中保存的map找到并返回URL对应的List<ConfigAttribute>。
package com.security.security;
import com.security.dao.PermissionDao;
import com.security.entity.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionDao permissionDao;
//此map缓存 URL与其权限关系
private volatile HashMap<String, Collection<ConfigAttribute>> map = null;
//在demo启动第一个用户登陆后,加载所有权限进map
//当DB中URL对应的权限发生变化时,可以调用此方法更新Security的url权限缓存map
//经测试方法执行后 实时生效
public void loadResourceDefine() {
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<Permission> permissions = permissionDao.findAll();
for (Permission permission : permissions) {
array = new ArrayList<>();
//此处只添加了用户的名字,其实还可以添加更多权限的信息,
//例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
cfg = new SecurityConfig(permission.getName());
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value
map.put(permission.getUrl(), array);
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) { //当DB中URL对应的权限发生变化时,也可以将map设置为null,触发重新加载权限
//重新加载
loadResourceDefine();
}
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
//遍历权限表中的url
for (String url : map.keySet()) {
matcher = new AntPathRequestMatcher(url);
//与request对比,符合则说明权限表中有该请求的URL
if(matcher.matches(request)) {
return map.get(url);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
总结:可以优先考虑自定义SecurityMetadataSource,因为SecurityMetadataSource是从BD加载并保存URL与权限的映射关系。HashMap<String, List<ConfigAttribute>>。且自定义的SecurityMetadataSource也会注入为Spring容器的Bean。在定义SecurityMetadataSource中增加一个重新加载HashMap的方法。只要能够控制这个方法就可以动态修改DB中的权限。
决策器:另外就是可以自定义AccessDecisionManager,官方的UnanimousBased其实足够使用,并且他是基于AccessDecisionVoter投票器来实现权限认证的,因此我们只需要自定义一个AccessDecisionVoter就可以了。可以选择直接将下面决策器实例注入,亦可继承下面决策器实现自定义决策器。或是实现AccessDecisionManager接口完全自定义一个支持全新逻辑的决策器。
Spring提供了3个决策管理器,至于这三个管理器是如何工作的请查看SpringSecurity源码
AffirmativeBased 一票通过,只要有一个投票器通过就允许访问
ConsensusBased 有一半以上投票器通过才允许访问资源
UnanimousBased 所有投票器都通过才允许访问
功能:通过上面SecurityMetadataSource提供的Collection<ConfigAttribute>和当前用户的Authentication进行比较鉴权
项目实例:自定义AccessDecisionManager未使用AccessDecisionVoter:https://gitee.com/-/ide/project/sw008/springbootdemo_source_code/edit/master/-/SpringSecurity/src/main/java/com/security/security/MyAccessDecisionManager.java
不要忘记实现AbstractSecurityInterceptor将自定义AccessDecisionManager或自定义SecurityMetadataSource注入到Security框架中。
项目实例实现AbstractSecurityInterceptor:https://gitee.com/-/ide/project/sw008/springbootdemo_source_code/edit/master/-/SpringSecurity/src/main/java/com/security/security/MyFilterSecurityInterceptor.java
项目实例说明:https://blog.csdn.net/weixin_43184769/article/details/84937685#t0
与CAS单点登陆结合
CAS项目实例:https://blog.csdn.net/shanchahua123456/article/details/85570647
本项目可直接与连接中的CAS单点登录项目结合。将本项目中自定义的AbstractSecurityInterceptor、AccessDecisionManager、SecurityMetadataSource直接放入cas项目实例项目中即可融合使用。使项目同时支持 CAS单点登陆认证+Security鉴权+DB动态配置URL对应权限。
SpringSecurity动态用户权限修改
每个用户都有自己的Authentication,其保存在SecurityContextHolder中。Authentication是通过SpringSecurity的UserDetial实现填充信息。
@GetMapping("/vip/test")
@Secured("ROLE_VIP") // 需要ROLE_VIP权限可访问
public String vipPath() {
return "仅 ROLE_VIP 可看";
}
@GetMapping("/vip")
public boolean updateToVIP() {
// 得到当前的认证信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 生成当前的所有授权
List<GrantedAuthority> updatedAuthorities = new ArrayList<>(auth.getAuthorities());
// 添加 ROLE_VIP 授权
updatedAuthorities.add(new SimpleGrantedAuthority("ROLE_VIP"));
// 生成新的认证信息
Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);
// 重置认证信息
SecurityContextHolder.getContext().setAuthentication(newAuth);
return true;
}
假设当前你的权限只有 ROLE_USER。那么按照上面的代码:
1、直接访问 /vip/test 路径将会得到403的Response;
2、访问 /vip 获取 ROLE_VIP 授权,再访问 /vip/test 即可得到正确的Response。
转自http://www.spring4all.com/article/155