Spring Security 动态加载URL权限

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>。

   项目实例:https://gitee.com/-/ide/project/sw008/springbootdemo_source_code/edit/master/-/SpringSecurity/src/main/java/com/security/security/MyInvocationSecurityMetadataSourceService.java

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
 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章