spring security将权限与分区相关联

本文介绍了如何在Spring Security中实现将权限与分区相关联,通过自定义GrantedAuthority、AccessDecisionVoter,以及在MethodSecurityInterceptor中的配置,确保用户请求的分区参数能用于权限判断。文章详细阐述了每个步骤的实现思路和代码示例,包括权限的创建、解析JWT Token、自定义AccessDecisionVoter以及解决权限冲突问题。
摘要由CSDN通过智能技术生成


前言

之前写过一个小型论坛,关于权限有一个需求就是在判断权限的时候经常还要判断分区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通过,我认为可以有这几种解决方案

  1. 不要让权限带有ROLE_前缀,但感觉有点不规范
  2. 把父类中accessDecisionManager代码抄一遍,但不添加RoleVoter,但因为prePostEnabled()等判断方法子类是无法调用的,所以有点麻烦,而且代码所表达的含有也不清楚
  3. 迭代AccessDecisionManager中的decisionVotersRoleVoter删了,不知道为什么感觉很蠢
  4. 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);
    }
    

}

总结

其实该有很多可以改的地方,例如应该写一个诸如GrantedAuthorityUtil之类的类,所以和自定义GrantedAuthority进行的交互都通过这个类中的方法来进行,还有PARTITION_ROLE_EDN_WITH参数不应该直接写死在类中。不过因为写这个只是为了学习spring security,写完功能没问题就懒得改了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值