本篇文章部分参考了https://www.ktanx.com/blog/p/4929
前言
上一篇文章中,我们已经实现了基于数据库的用户认证,接下来要做的就是基于数据库的角色授权,即动态的权限验证,实现Spring Security的决策管理器AccessDecisionManager。
要实现动态的权限验证,当然要先有对应的访问权限资源了。Spring Security是通过SecurityMetadataSource来加载访问时所需要的具体权限,所以第一步需要实现SecurityMetadataSource。
SecurityMetadataSource是一个接口,同时还有一个接口FilterInvocationSecurityMetadataSource继承于它,但FilterInvocationSecurityMetadataSource只是一个标识接口,对应于FilterInvocation,本身并无任何内容:
/**
* Marker interface for <code>SecurityMetadataSource</code> implementations that are
* designed to perform lookups keyed on {@link FilterInvocation}s.
*
* @author Ben Alex
*/
public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {
}
因为我们做的一般都是web项目,所以实际需要实现的接口是FilterInvocationSecurityMetadataSource,这是因为Spring Security中很多web才使用的类参数类型都是FilterInvocationSecurityMetadataSource。
下面我们自定义实现类CustomInvocationSecurityMetadataSource,它的主要责任就是当访问一个url时返回这个url所需要的访问权限。
一、新增CustomInvocationSecurityMetadataSource类实现FilterInvocationSecurityMetadataSource接口
*
* @author ZHANGCHAO
* @date 2020/3/18 8:06
* @since 1.0.0
*/
@Slf4j
@Service
public class CustomInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private ISysResourceService resourceService;
@Autowired
private ISysRoleResourceService roleResourceService;
@Autowired
private ISysRoleService roleService;
/**
* key是url+method
* value是对应url资源的角色列表
**/
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();
/**
* 组装资源路径和其对应的权限列表map
* 注意:
* @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
* 此方法必须在将类放入服务之前调用,且只执行一次。
*
* @Author ZHANGCHAO
* @Date 2020/3/18 14:06
* @param
* @retrun void
**/
@PostConstruct
public void init(){
log.info("[自定义权限资源数据源]:{}","初始化权限资源");
List<SysResource> resourceList = (List<SysResource>) resourceService.list().getData();
resourceList.forEach(sysResource -> {
List<SysRoleResource> roleResources = (List<SysRoleResource>) roleResourceService.list(new QueryWrapper<SysRoleResource>().lambda().eq(SysRoleResource::getResourceCode,sysResource.getCode())).getData();
if (isNotEmpty(roleResources)){
List<String> rolesNames = new ArrayList<>();
roleResources.forEach(sysRoleResource -> {
SysRole role = (SysRole) roleService.getOne(new QueryWrapper<SysRole>().lambda().eq(SysRole::getCode,sysRoleResource.getRoleCode())).getData();
rolesNames.add(role.getName());
});
if (isNotEmpty(rolesNames)){
List<ConfigAttribute> configAttributes = new ArrayList<>();
rolesNames.forEach(roleName -> configAttributes.add(new SecurityConfig(roleName)));
// url == 权限列表
requestMap.put(new AntPathRequestMatcher(sysResource.getUrl()),configAttributes);
}
}
});
log.info("requestMap:==> "+requestMap);
}
/**
* getAttributes方法返回本次访问需要的权限,可以有多个权限。
* 在上面的实现中如果没有匹配的url直接返回null,
* 也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。
*
* 访问配置属性(ConfigAttribute)用于给定安全对象(通过的验证)
*
* @param o 安全的对象
* @return 用于传入的安全对象的属性。 如果没有适用的属性,则应返回空集合。
* @throws IllegalArgumentException 如果传递的对象不是SecurityDatasource实现支持的类型,则抛出异常
**/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
log.info("[自定义权限资源数据源]:{}","获取本次访问需要的权限");
if (requestMap.isEmpty()){
init();
}
FilterInvocation fi = (FilterInvocation) o;
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(fi.getHttpRequest())) {
log.info("[自定义权限资源数据源]:当前路径[{}]需要的资源权限:[{}] ==> 触发鉴权决策管理器",entry.getKey(),entry.getValue().toString());
return entry.getValue();
}
}
log.info("[自定义权限资源数据源]:{}==> {}","白名单路径",fi.getHttpRequest().getRequestURI());
return null;
}
/**
* getAllConfigAttributes方法如果返回了所有定义的权限资源,
* Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。
*
* 如果可用,则返回由实现类定义的所有ConfigAttribute。
*
* AbstractSecurityInterceptor使用它对针对它ConfigAttribute的每个配置属性执行启动时验证。
*
* @return ConfigAttribute,如果没有适用的,就返回null
**/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
List<ConfigAttribute> allAttributes = new ArrayList<>();
requestMap.forEach((key,value) -> {allAttributes.addAll(value);});
log.info("[自定义权限资源数据源]:获取所有的角色==> {}",allAttributes.toString());
return allAttributes;
}
/**
* AbstractSecurityInterceptor 调用
* supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
*
* @param aClass 正在查询的类
* @return 如果实现可以处理指定的类,则为true
*/
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
注意:@PostConstruct注解的方法将会在依赖注入完成后被自动调用。
getAttributes方法返回本次访问需要的权限,可以有多个权限。在上面的实现中如果没有匹配的url直接返回null,也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。
getAllConfigAttributes方法如果返回了所有定义的权限资源,Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。
supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
在加载的时候,这里的url是key,value是访问需要的权限码,一个权限码可以对应多个url,一个url也可以有多个权限码,想要怎么玩都可以在这里实现,示例中只是最简单的。
PS: 另外说一下FilterInvocation类:
这个类的作用本身很简单,就是把doFilter传进来的request,response和FilterChain对象保存起来,供FilterSecurityInterceptor的处理代码调用。
一般写FilterSecurityInterceptor类的代码时都会直接把doFilter的参数要么保存在FilterSecurityInterceptor类的相关属性里,要么就是直接传进来用,并且不断地在各个方法中传递这些参数。由此可见springSecurity的作者这个小小的设计使得代码的可阅读性和藕合性大大降低,因为FilterInvocation类替代了这些参数在FilterSecurityInterceptor类中各处游动,,这样通过该类屏蔽了web filter过滤器环境。
有了权限资源,知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了。这需要通过实现自定义的AccessDecisionManager来实现。Spring Security内置的几个AccessDecisionManager就不讲了,在web项目中基本用不到。
二、自定义CustomAccessDecisionManager 类,实现AccessDecisionManager接口
重写权限决策,根据URL资源权限和用户角色,进行鉴权。
/**
* 权限决策管理器
*
* @author ZHANGCHAO
* @date 2020/3/18 16:52
* @since 1.0.0
*/
@Slf4j
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {
/**
* 权限鉴定
*
* @param authentication from SecurityContextHolder.getContext() => userDetails.getAuthorities()
* @param o 就是FilterInvocation对象,可以得到request等web资源。
* @param collection from MetaDataSource.getAttributes(),已经被框架做了非空判断
* @throws AccessDeniedException 如果由于身份验证不具有所需的权限或ACL特权而拒绝访问
* @throws InsufficientAuthenticationException 如果由于身份验证没有提供足够的信任级别而拒绝访问
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
log.info("****************************************权限鉴定********************************************");
/*FilterInvocation filterInvocation = (FilterInvocation) object; // object 是一个URL
log.info("[当前路径[{}]需要的资源权限]:{}",filterInvocation.getRequestUrl(),configAttributes);*/
log.info("[登录用户[{}]权限]:{}",authentication.getName(),authentication.getAuthorities());
if(collection == null){
return;
}
for (ConfigAttribute configAttribute : collection) {
// 资源的权限
String attribute = configAttribute.getAttribute();
// 当前用户的权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities){
if (attribute.equals(authority.getAuthority())){
log.info("[权限决策管理器]:登录用户[{}]权限匹配=[{}]",authentication.getName(),attribute);
return;
}
}
}
log.info("[权限决策管理器]:登录用户[{}]权限不足",authentication.getName());
throw new AccessDeniedException("权限不足");
}
/**
* AbstractSecurityInterceptor 调用,遍历ConfigAttribute集合,筛选出不支持的attribute
*
* @param configAttribute a configuration attribute that has been configured against the
* <code>AbstractSecurityInterceptor</code>
* @return true if this <code>AccessDecisionManager</code> can support the passed
* configuration attribute
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* AbstractSecurityInterceptor 调用,验证AccessDecisionManager是否支持这个安全对象的类型。
* supports(Class)方法被安全拦截器实现调用,
* 包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。
*
* @param aClass the class that is being queried
* @return <code>true</code> if the implementation can process the indicated class
*/
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
同样的也有三个方法,其它两个和SecurityMetadataSource
类似,这里主要讲讲decide
方法。
decide方法的三个参数中:
- authentication包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
- object就是FilterInvocation对象,可以得到request等web资源。
- collection是本次访问需要的权限。
上面的实现中,当需要多个权限时只要有一个符合则校验通过,即或
的关系,想要并
的关系只需要修改这里的逻辑即可。
三、自定义CustomFilterSecurityInterceptor,继承AbstractSecurityInterceptor
/**
* @author ZHANGCHAO
* @date 2020/3/19 11:32
* @since 1.0.0
*/
@Slf4j
@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private CustomInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
private CustomAccessDecisionManager accessDecisionManager;
private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";
/**
* 初始化时将定义的DecisionManager,注入到父类AbstractSecurityInterceptor中
* 注意:
* @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
* 此方法必须在将类放入服务之前调用,且只执行一次。
*/
@PostConstruct
public void init() {
log.info("设置==========================================鉴权决策管理器");
super.setAccessDecisionManager(accessDecisionManager);
}
/**
* 向父类提供要处理的安全对象类型,因为父亲被调用的方法参数类型大多是Object,框架需要保证传递进去的安全对象类型相同
*
* @return 子类为其提供服务的安全对象的类型
*/
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
/**
* 获取到自定义MetadataSource的方法
*
* 启动时会有3次调用
* 第一次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 135行
* 第二次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 137行
* 第三次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 156行
*
* 登录时调用
* 调用:{@link AbstractSecurityInterceptor#beforeInvocation(Object)} 196行
*
* @return 权限资源映射的数据源
*/
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
/**
*
* 由Web容器调用,以向filter指示正在将其放入服务中。
* servlet容器在实例化filter后,只调用一个init方法。
* 在要求filter执行任何过滤之前,init方法必须成功完成。
* 如果init方法满足以下条件之一,则web容器无法将筛选器放入服务:抛出 servletException
* 在web容器定义的时间内不返回
* 默认实现时NO-OP
* @param filterConfig 与正在初始化的filter实例关联的配置信息
* @throws ServletException 如果实例化失败
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("filer==========================================init");
}
/**
* 每当request/response对由于客户端请求链末端的资源而通过链时,容器调用过滤器的doFilter方法。
* 传入此方法的filter chain 允许Filter传递请求并响应链中的下一个实体。
*
* 此方法的典型实现将遵循以下模式:
* 1.检查请求
* 2.也可以使用自定义实现包装请求对象,输入filter的内容或头
* 3.(可选)使用自定义实现包装响应对象,以Filter 内容或头进行输出过滤
* 4.使用FilterChain对象的chain.doFilter()调用链中的下一个实体
* 5.在调用FilterChain中的下一个实体后,直接在响应上设置头。
* @param request 要处理的请求
* @param response 与请求关联的响应
* @param filterChain 提供对链中下一个Filter的访问,以便此Filter将请求和响应传递给以进行进一步处理
* @throws IOException 如果在此筛选器处理请求期间发生I/O错误
* @throws ServletException 如果由于其他原因处理失败
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
log.info("[自定义过滤器]:{}","CustomFilterSecurityInterceptor.doFilter()");
FilterInvocation filterInvocation = new FilterInvocation(request,response,filterChain);
invoke(filterInvocation);
}
/**
* 由Web容器调用,以向filter指示它正在退出服务。
* 只有当filter的doFilter方法中的所有线程都退出或超出时间间后,才调用此方法。
* 在web容器调用此方法之后,它将不再在此filter的实例上调用doFilter方法。
* 此方法使filter有机会clean正在保留的任何资源(例如内存、文件句柄、线程)
* 并确保任何持久状态与filter在内存中的当前状态同步。
*
* 默认实现时NO-OP
*/
@Override
public void destroy() {
log.info("filer==========================================destroy");
}
private void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (filterInvocation.getRequest() != null && filterInvocation.getRequest().getAttribute(FILTER_APPLIED) != null) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(),filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 调用父类的beforeInvocation ==> accessDecisionManager.decide(..)
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(),filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token,null);
}
}
}
抽象类AbstractSecurityInterceptor默认实现是FilterSecurityInterceptor,进行访问资源时,会通过这个拦截器拦截访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截, 其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息, 还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等), 如果权限足够,则返回,权限不够则报错并调用权限不足页面。
AbstractSecurityInterceptor这个过滤器基本上控制着spring security 的整个流程。Spring Security核心流程具体每条的实现由AbstractSecurityInterceptor 实现,也就是说AbstractSecurityInterceptor 只是定义了一些行为,然后这些行为的安排,也就是执行流程则是由具体的子类所实现,AbstractSecurityInterceptor 虽然也叫Interceptor ,但是并没有继承和实现任何和过滤器相关的类,具体和过滤器有关的部分是由子类所定义。每一种受保护对象都拥有继承自AbstrachSecurityInterceptor的拦截器类。spring security 提供了两个具体实现类,MethodSecurityInterceptor 将用于受保护的方法,FilterSecurityInterceptor 用于受保护的web 请求。
AbstractSecurityInterceptor 的两个实现都具有一致的逻辑:
- 先将正在请求调用的受保护对象传递给beforeInvocation()方法进行权限鉴定。
- 权限鉴定失败就直接抛出异常了。
- 鉴定成功将尝试调用受保护对象,调用完成后,不管是成功调用,还是抛出异常,都将执行finallyInvocation()。
- 如果在调用受保护对象后没有抛出异常,则调用afterInvocation()。
AbstractSecurityInterceptor 中的一些方法:
- afterPropertiesSet():主要是对类的属性进行校验。
- beforeInvocation():这个方法实现了对访问对象的权限校验,内部使用了AccessDecisionManager 和 AuthenticationManager。
- finallyInvocation():这个方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()的方法。
- finallyInvocation():方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeInvocation()中改变了SecurityContext,则在finallyInvocation()中需要将其恢复为原来的SecurityContext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中。
注意:在spring容器托管的AbstractSecurityInterceptor的bean,都会自动加入到servlet的filter chain,不用在websecurityconfig配置
四、启动项目测试
以上