spring security将权限与分区相关联
前言
之前写过一个小型论坛,关于权限有一个需求就是在判断权限的时候经常还要判断分区id是否匹配, 某个角色的权限只在其对应的分区有效,学习spring security后试着用spring security实现这个需求一、大致思路
因为判断是需要用户请求的分区参数,所以选择方法层面的权限管理,用户拥有的权限要保存分区信息所有要创建自己的GrantedAuthority,提供自定义AccessDecisionVoter的实现类,获取请求参数中的分区信息,然后在进行判断。
二、自定义GrantedAuthority
将角色名作为权限,若角色名带有_PARTITION后缀认为角色权限与分区关联,partitionId属性有效
代码如下(示例):
@Data
public class PartitionGrantedAuthority implements GrantedAuthority {
public static final String PARTITION_ROLE_EDN_WITH = "_PARTITION";
private String authority;
private String partitionId = "";
/**
* 将PartitionGrantedAuthority实体类转化为符合PartitionGrantedAuthority格式的字符串
* @param partitionGrantedAuthority PartitionGrantedAuthority
* @return 字符串
*/
public static String toFormatStr(PartitionGrantedAuthority partitionGrantedAuthority){
return partitionGrantedAuthority.authority + "-" + partitionGrantedAuthority.partitionId;
}
/**
* 将格式合理的字符串转化为对应的PartitionGrantedAuthority实体类
* @param authorityStrWithPartition 符合格式的字符串authority与partitionId使用 - 相间隔
* @return authorityStrWithPartition
*/
public static PartitionGrantedAuthority fromFormatStr(String authorityStrWithPartition){
String[] strings = authorityStrWithPartition.split("-");
if(strings.length != 2){
throw new RuntimeException("字符串格式错误");
}
PartitionGrantedAuthority authority = new PartitionGrantedAuthority();
authority.authority = strings[0];
authority.partitionId = strings[1];
return authority;
}
public PartitionGrantedAuthority(){
super();
}
public PartitionGrantedAuthority(Role role){
authority = role.getName();
if(authority.endsWith(PARTITION_ROLE_EDN_WITH))
partitionId = role.getPartitionId();
}
}
修改从roles转化为authoritis的代码
roles.stream()
.map(PartitionGrantedAuthority::new)
.collect(Collectors.toList());
再修改生成和解析jwtToken时的代码,保证生成和解析token时partitionId这个属性不会丢失
三. 自定义AccessDecisionVoter的实现
要求分区权限的方法同时要求提供的参数中包含分区信息,通过反射来获取。具体实现基本可以照抄RoleVoter只是进行额外的判断
代码如下(示例):
public class PartitionVoter implements AccessDecisionVoter<MethodInvocation> {
private static final String PARTITION_ID_NULL = "null";
private static final String PARTITION_ID_FILED_NAME = "partitionId";
public PartitionVoter() {
}
@Override
public boolean supports(ConfigAttribute attribute) {
return (attribute.getAttribute() != null)
&& attribute.getAttribute().endsWith(PartitionGrantedAuthority.PARTITION_ROLE_EDN_WITH);
}
@Override
public boolean supports(Class<?> clazz) {
return MethodInvocation.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, MethodInvocation mi, Collection<ConfigAttribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute attribute : attributes) {
//能进行处理的所需权限
if (this.supports(attribute)) {
// 拥有的权限
for (GrantedAuthority authority : authorities) {
if(authority instanceof PartitionGrantedAuthority){
result = ACCESS_DENIED;
String partition = getPartitionId(mi);
PartitionGrantedAuthority partitionGrantedAuthority = (PartitionGrantedAuthority) authority;
//判断请求中附带的PartitionId
if (partitionGrantedAuthority.getPartitionId().equals(partition) //拥有权限的partitionId与请求处理的partitionId是否匹配
&& attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
}
return result;
}
/**
* 从调用方法的实参中获取PartitionId
* 要求表示partitionId的域的name值为partitionId
*
* @param mi mi
* @return 返回PARTITION_NAME_NULL表示参数中无partitionId信息
*/
private String getPartitionId(MethodInvocation mi) {
Object[] arguments = mi.getArguments();
String res = PARTITION_ID_NULL;
for (Object o : arguments) {
Class<?> c = o.getClass();
try {
Field field = c.getDeclaredField(PARTITION_ID_FILED_NAME);
field.setAccessible(true);
res = (String) field.get(o);
} catch (NoSuchFieldException ignored) {
//没有对应域返回PARTITION_ID_NULL
} catch (IllegalAccessException e) {
log.warn("发生异常", e);
return res;
}
}
return res;
}
}
四.设置方法需要的权限
本来是想复用PreAuthorize的,但是它的解析过程实在没办法下手,所以使用已有@Secured
注解标注需要的权限即就可被PartitionVoter处理。
问题:因为默认的DelegatingMethodSecurityMetadataSource实现方式导致同时配置@PreAuthorize
和@Secured
注解会导致后者的配置无法读取,想让二者同时生效必须完全重写methodSecurityMetadataSource()
方法。
五.把这些配置到MethodSecurityInterceptor
自定义GlobalMethodSecurityConfiguration的子类MethodSecurityConfig,并添加@EnableGlobalMethodSecurity
注解,如果
SecurityConfig上也有@EnableGlobalMethodSecurity
注解会导致MethodSecurityConfiguration失效,不知道为什么。
还有一个小问题就是默认的AccessDecisionManager的实现是包含RoleVoter的,所以如果标注的权限是以ROLE_开头会被RoleVoter通过,我认为可以有这几种解决方案
- 不要让权限带有ROLE_前缀,但感觉有点不规范
- 把父类中accessDecisionManager代码抄一遍,但不添加RoleVoter,但因为prePostEnabled()等判断方法子类是无法调用的,所以有点麻烦,而且代码所表达的含有也不清楚
- 迭代AccessDecisionManager中的decisionVoters把RoleVoter删了,不知道为什么感觉很蠢
- 把AccessDecisionManagerd的实现改为UnanimousBased,让PartitionVoter拥有一票否决权,如果遇见一定不能设置一票否决的情况就不行了
我为了方便选择最后一种
代码如下(示例):
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Bean
@Override
protected AccessDecisionManager accessDecisionManager() {
AbstractAccessDecisionManager decisionManager = (AbstractAccessDecisionManager)super.accessDecisionManager();
List<AccessDecisionVoter<?>> decisionVoters = decisionManager.getDecisionVoters();
decisionVoters.add(new PartitionVoter());
return new UnanimousBased(decisionVoters);
}
}