我們接着上一章(四)Spring Security基於數據庫的用戶認證,進行開發
一:重寫並實現了基於數據庫的權限數據源
/**
* 權限資源
* FilterInvocationSecurityMetadataSource的默認實現是
* DefaultFilterInvocationSecurityMetadataSource
*/
@Service
public class CustomInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
private static final Logger log = LoggerFactory.getLogger(CustomInvocationSecurityMetadataSourceService.class);
private final PermissionRepository permissionRepository;
/* key 是url+method ,value 是對應url資源的角色列表 */
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();
@Autowired
public CustomInvocationSecurityMetadataSourceService(PermissionRepository permissionRepository) {
this.permissionRepository = permissionRepository;
}
/**
* 注意:
* @PostConstruct 用於在依賴關係注入完成之後需要執行的方法,以執行任何初始化。
* 此方法必須在將類放入服務之前調用,且只執行一次。
*/
@PostConstruct
public void init(){
log.info("[自定義權限資源數據源]:{}","初始化權限資源");
List<Permission> permissions = permissionRepository.findAll();
permissions.forEach(item->{
Set<String> roleNames = item.getRoleNames();
List<ConfigAttribute> configAttributes = new ArrayList<>();
for (String roleName : roleNames) {
configAttributes.add(new SecurityConfig(roleName));
}
requestMap.put(new AntPathRequestMatcher(item.getUrl()),configAttributes);
});
System.out.println(requestMap.toString());
}
/**
* getAttributes方法返回本次訪問需要的權限,可以有多個權限。
* 在上面的實現中如果沒有匹配的url直接返回null,
* 也就是沒有配置權限的url默認都爲白名單,想要換成默認是黑名單隻要修改這裏即可。
*
* 訪問配置屬性(ConfigAttribute)用於給定安全對象(通過的驗證)
*
* @param object 安全的對象
* @return 用於傳入的安全對象的屬性。 如果沒有適用的屬性,則應返回空集合。
* @throws IllegalArgumentException 如果傳遞的對象不是SecurityDatasource實現支持的類型,則拋出異常
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
log.info("[自定義權限資源數據源]:{}","獲取本次訪問需要的權限");
if(requestMap.isEmpty()){
init();
}
final HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(request)) {
log.info("[自定義權限資源數據源]:當前路徑[{}]需要的資源權限:[{}] ==> 觸發鑑權決策管理器",entry.getKey(),entry.getValue().toString());
return entry.getValue();
}
}
log.info("[自定義權限資源數據源]:{}==> {}","白名單路徑",request.getRequestURI());
return null;
}
/**
*
*
* getAllConfigAttributes方法如果返回了所有定義的權限資源,
* Spring Security會在啓動時校驗每個ConfigAttribute是否配置正確,不需要校驗直接返回null。
*
*
* 如果可用,則返回由實現類定義的所有ConfigAttribute。
*
* AbstractSecurityInterceptor使用它對針對它ConfigAttribute的每個配置屬性執行啓動時驗證。
*
* @return ConfigAttribute,如果沒有適用的,就返回null
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<>();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
allAttributes.addAll(entry.getValue());
}
log.info("[自定義權限資源數據源]:獲取所有的角色==> {}",allAttributes.toString());
return allAttributes;
}
/**
* AbstractSecurityInterceptor 調用
* supports方法返回類對象是否支持校驗,web項目一般使用FilterInvocation來判斷,或者直接返回true。
*
* @param clazz 正在查詢的類
* @return 如果實現可以處理指定的類,則爲true
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
二:重寫權限決策
根據URL資源權限和用戶角色,進行鑑權
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {
private static final Logger log = LoggerFactory.getLogger(CustomAccessDecisionManager.class);
/**
* 權限鑑定
*
* @param authentication from SecurityContextHolder.getContext() => userDetails.getAuthorities()
* @param object 就是FilterInvocation對象,可以得到request等web資源。
* @param configAttributes from MetaDataSource.getAttributes(),已經被框架做了非空判斷
* @throws AccessDeniedException 如果由於身份驗證不具有所需的權限或ACL特權而拒絕訪問
* @throws InsufficientAuthenticationException 如果由於身份驗證沒有提供足夠的信任級別而拒絕訪問
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
log.info("****************************************權限鑑定********************************************");
/*FilterInvocation filterInvocation = (FilterInvocation) object; // object 是一個URL
log.info("[當前路徑[{}]需要的資源權限]:{}",filterInvocation.getRequestUrl(),configAttributes);*/
log.info("[登錄用戶[{}]權限]:{}",authentication.getName(),authentication.getAuthorities());
if(configAttributes == null){
return;
}
for (ConfigAttribute configAttribute : configAttributes) {
/* 資源的權限 */
String attribute = configAttribute.getAttribute();
/* 用戶的權限 */
for (GrantedAuthority authority : authentication.getAuthorities()) { // 當前用戶的權限
if(authority.getAuthority().trim().equals("ROLE_ANONYMOUS"))return;
log.info("[資源角色==用戶角色] ? {} == {}", attribute.trim(), authority.getAuthority().trim());
if (attribute.trim().equals(authority.getAuthority().trim())) {
log.info("[鑑權決策管理器]:登錄用戶[{}]權限匹配",authentication.getName());
return;
}
}
}
log.info("[鑑權決策管理器]:登錄用戶[{}]權限不足",authentication.getName());
throw new AccessDeniedException("權限不足");
}
/**
* AbstractSecurityInterceptor 調用,遍歷ConfigAttribute集合,篩選出不支持的attribute
*
* @param attribute a configuration attribute that has been configured against the
* <code>AbstractSecurityInterceptor</code>
* @return true if this <code>AccessDecisionManager</code> can support the passed
* configuration attribute
*/
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
/**
* AbstractSecurityInterceptor 調用,驗證AccessDecisionManager是否支持這個安全對象的類型。
* supports(Class)方法被安全攔截器實現調用,
* 包含安全攔截器將顯示的AccessDecisionManager支持安全對象的類型。
*
* @param clazz the class that is being queried
* @return <code>true</code> if the implementation can process the indicated class
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
三:實現AbstractSecurityInterceptor
默認實現是FilterSecurityInterceptor,進行訪問資源時,會通過這個攔截器攔截
訪問資源(即授權管理),訪問url時,會通過AbstractSecurityInterceptor攔截器攔截, 其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,在調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權限信息, 還會獲取被攔截的url和被攔截url所需的全部權限,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等), 如果權限足夠,則返回,權限不夠則報錯並調用權限不足頁面
@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private static final Logger log = LoggerFactory.getLogger(CustomFilterSecurityInterceptor.class);
private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";
private final CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService;
private final CustomAccessDecisionManager customAccessDecisionManager;
@Autowired
public CustomFilterSecurityInterceptor(CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService, CustomAccessDecisionManager customAccessDecisionManager) {
this.customInvocationSecurityMetadataSourceService = customInvocationSecurityMetadataSourceService;
this.customAccessDecisionManager = customAccessDecisionManager;
}
/**
* 初始化時將定義的DecisionManager,注入到父類AbstractSecurityInterceptor中
* 注意:
* @PostConstruct 用於在依賴關係注入完成之後需要執行的方法,以執行任何初始化。
* 此方法必須在將類放入服務之前調用,且只執行一次。
*/
@PostConstruct
public void init(){
log.info("設置==========================================鑑權決策管理器");
super.setAccessDecisionManager(customAccessDecisionManager);
}
/**
* 向父類提供要處理的安全對象類型,因爲父親被調用的方法參數類型大多是Object,框架需要保證傳遞進去的安全對象類型相同
*
* @return 子類爲其提供服務的安全對象的類型
*/
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
/**
* 獲取到自定義MetadataSource的方法
*
* 啓動時會有3次調用
* 第一次調用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 135行
* 第二次調用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 137行
* 第三次調用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 156行
*
* 登錄時調用
* 調用:{@link AbstractSecurityInterceptor#beforeInvocation(Object)} 196行
*
* @return 權限資源映射的數據源
*/
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.customInvocationSecurityMetadataSourceService;
}
/**
*
* 由Web容器調用,以向filter指示正在將其放入服務中。
* servlet容器在實例化filter後,只調用一個init方法。
* 在要求filter執行任何過濾之前,init方法必須成功完成。
* 如果init方法滿足以下條件之一,則web容器無法將篩選器放入服務:拋出 servletException
* 在web容器定義的時間內不返回
* 默認實現時NO-OP
* @param filterConfig 與正在初始化的filter實例關聯的配置信息
* @throws ServletException 如果實例化失敗
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("filer==========================================init");
}
/**
* 每當request/response對由於客戶端請求鏈末端的資源而通過鏈時,容器調用過濾器的doFilter方法。
* 傳入此方法的filter chain 允許Filter傳遞請求並響應鏈中的下一個實體。
*
* 此方法的典型實現將遵循以下模式:
* 1.檢查請求
* 2.也可以使用自定義實現包裝請求對象,輸入filter的內容或頭
* 3.(可選)使用自定義實現包裝響應對象,以Filter 內容或頭進行輸出過濾
* 4.使用FilterChain對象的chain.doFilter()調用鏈中的下一個實體
* 5.在調用FilterChain中的下一個實體後,直接在響應上設置頭。
* @param request 要處理的請求
* @param response 與請求關聯的響應
* @param chain 提供對鏈中下一個Filter的訪問,以便此Filter將請求和響應傳遞給以進行進一步處理
* @throws IOException 如果在此篩選器處理請求期間發生I/O錯誤
* @throws ServletException 如果由於其他原因處理失敗
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("[自定義過濾器]:{}","CustomFilterSecurityInterceptor.doFilter()");
FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
invoke(filterInvocation);
}
/**
* 由Web容器調用,以向filter指示它正在退出服務。
* 只有當filter的doFilter方法中的所有線程都退出或超出時間間後,才調用此方法。
* 在web容器調用此方法之後,它將不再在此filter的實例上調用doFilter方法。
* 此方法使filter有機會clean正在保留的任何資源(例如內存、文件句柄、線程)
* 並確保任何持久狀態與filter在內存中的當前狀態同步。
*
* 默認實現時NO-OP
*/
@Override
public void destroy() {
log.info("filer==========================================destroy");
}
private void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null ) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
/* 調用父類的beforeInvocation ==> accessDecisionManager.decide(..) */
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
}
注意:在spring容器託管的AbstractSecurityInterceptor的bean,都會自動加入到servlet的filter chain,不用在websecurityconfig配置
示例
當權限不足時,會自動跳轉到403