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