Spring security默认是在代码里约定好权限,真实的业务场景通常需要可以支持动态配置角色访问权限,即在运行时去配置url对应的访问角色。
最简单的方法就是自定义一个Filter去完成权限判断,但这脱离了spring security
框架,如何基于spring security优雅的实现呢? 其实只要仔细看看
FilterSecurityInterceptor`源码就知道从哪里找切入点了
要想实现动态权限 需要以下3步
- 实现
FilterInvocationSecurityMetadataSource
获取请求Url需要的角色列表 - 自定义投票器 实现
AccessDecisionVoter
或者 直接重写AccessDecisionManager
不使用SpringSecurity
提供的默认访问策略逻辑 - 定义处理 访问无权限的handler 实现
AccessDeniedHandler
在这里可以返回给前端 对应的信息
1.FilterSecurityInterceptor
该过滤器实现了主要的鉴权逻辑,最核心的代码如下
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
抽取重要的2部分
1.1 获取请求的Url的所需要的 角色Collection
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
private FilterInvocationSecurityMetadataSource securityMetadataSource;
1.2 使用访问决策管理器 去 鉴权
this.accessDecisionManager.decide(authenticated, object, attributes);
那么我们就从上面两部分进行切入
2.自定义实现 FilterInvocationSecurityMetadataSource接口
FilterInvocationSecurityMetadataSource 提供一个 getAttributes方法 用来获取请求的URL对应需要的角色 下面的代码中,我模拟从数据库取出 路径对应的角色urlRoleMap
可以通过 FilterInvocation获取到 请求的url 动态从数据库中获取 改url需要的角色列表,然后放入Collection ,而它是 SecurityConfig
@Slf4j
@Component
public class RoleSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//根据 请求获取 需要的权限
FilterInvocation filterInvocation = (FilterInvocation) object;
String url = filterInvocation.getRequestUrl();
log.info("【请求 url : {}】", url);
Map<String, List<String>> urlRoleMap = new HashMap();
urlRoleMap.put("/menu/**", Arrays.asList("admin","editor1"));
urlRoleMap.put("/user/listByCondition**", Arrays.asList("admin1","editor1"));
for (Map.Entry<String, List<String>> entry : urlRoleMap.entrySet()) {
if (antPathMatcher.match(entry.getKey(), url)) {
String[] array = entry.getValue().toArray(new String[0]);
return SecurityConfig.createList(array);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
最后需要把该 自定义的元数据获取类 配置到SpringSecurity 中 通过 withObjectPostProcessor
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setSecurityMetadataSource(roleSecurityMetadataSource);
return fsi;
}
})
3.提供自定义的 投票器Voter
默认 SpringSecurity 提供了3中访问决策 逻辑
AffirmativeBased – 任何一个AccessDecisionVoter返回同意则允许访问
ConsensusBased – 同意投票多于拒绝投票(忽略弃权回答)则允许访问
UnanimousBased – 每个投票者选择弃权或同意则允许访问我也可以选择 自定义实现AccessDecisionManager ,但是SpringSecurity提供的已经足够用了
所以我们选择 第一种,也是SpringSecurity 默认使用的 AffirmativeBased
3.1 使用 AffirmativeBased 作为 决策访问策略
主要就是遍历实现了AccessDecisionVoter 接口的 投票器,让投票器决定返回结果 支持3种返回结果
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
3.2 自定义 投票器 实现AccessDecisionVoter
其中入参 authentication 表示当前的认证用户信息, Object object 是指FilterInvocation Collection attributes 是指 FilterInvocationSecurityMetadataSource返回的当前Url请求需要的角色集合
/**
* 自定义的 投票器
*
* @author johnny
* @create 2020-07-19 上午12:41
**/
public class RoleBasedVoter implements AccessDecisionVoter<Object> {
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute attribute : attributes) {
if (attribute.getAttribute() == null) {
continue;
}
//默认的SpringSecurity的投票器,比如RoleVoter 的 support方法会去判断角色是否 包含ROLE_前缀
//我们这里不做这种限制
if (this.supports(attribute)) {
for (GrantedAuthority authority : authorities) {
if(attribute.getAttribute().equals(authority.getAuthority())){
return ACCESS_GRANTED;
}
}
}
}
return ACCESS_DENIED;
}
}
4.SpringSecurity 配置装载
本配置是在 前后端分离情况下 包含登录、退出、以及无Session状态 和 自定义 JwtTokenFilter ,动态权限等等
@Autowired
private FilterInvocationSecurityMetadataSource roleSecurityMetadataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.formLogin()
.loginPage("/auth/login")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailHandler)
.and()
.logout()
.logoutUrl("/user/logout")
.clearAuthentication(true)
.logoutSuccessHandler(logOutSuccessHandler)
.and()
.authorizeRequests()
.anyRequest().authenticated()
//1.放入修改后的accessDecisionManager
.accessDecisionManager(customizeAccessDecisionManager())
//2.扩展 FilterSecurityInterceptor,放入自定义的FilterInvocationSecurityMetadataSource
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setSecurityMetadataSource(roleSecurityMetadataSource);
return fsi;
}
})
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(customizeAuthenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
http.addFilterAfter(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
//使用自定义角色器,放入 AccessDecisionManager的一个实现 AffirmativeBased 中
private AccessDecisionManager customizeAccessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoterList
= Arrays.asList(
new RoleBasedVoter()
);
return new AffirmativeBased(decisionVoterList);
}
5.总结
本篇主要讲解 SpringSecurity中如何动态权限校验,粒度为请求级别校验,主要过滤器为 FilterSecurityInterceptor 去进行权限校验
涉及到FilterInvocationSecurityMetadataSource
获取Url对应的角色信息 , AccessDecisionManager
真正进行访问决策的 以及 AccessDecisionVoter
进行投票的投票器 等 找准切入点 即可很轻松实现。