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
 

 

 

 

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