Spring Security 實現 antMatchers 配置路徑的動態獲取
1. 為什麼要實現動態的獲取 antMatchers 配置的數據
這兩天由於公司項目的需求,對 spring security 的應用過程中需要實現動態的獲取 antMatchers ,permitAll , hasAnyRole , hasIpAddress 等這些原本通過硬編碼的方式配置的數據。為了讓每一個業務服務不用再去處理許可權驗證等這些和業務無關的邏輯,而是只專註於它所負責的業務,就要將認證、授權統一的放在 API 網關層去處理。但是每個不同的業務服務有的介面需要認證後才能訪問,有的介面是不需要認證就可以訪問的,有的介面可能是需要某些許可權、角色才可以訪問。這樣依賴 API 網關就必須知道並且能夠區分出來每個業務服務的介面哪些是需要認證後才可以訪問的,那些介面是不需要經過認證就可以訪問的。 為了實現這個功能 spring security 提供的 antMatchers 函數硬編碼的方式就不適用了。而是應該提供一個管理端,每個業務服務把他們這些個性化的介面通過管理端去進行配置,統一的存儲起來,spring security 在獲取這些數據的時候從統一的存儲中來獲取這些數據。基於這個需求前提我來考慮如何實現這個功能。配套視頻講解地址 :http://www.iqiyi.com/w_19s456x5b5.html?pltfm=11&pos=title&flashvars=videoIsFromQidan%3Ditemviewclk_a#vfrm=5-6-0-1
2. 從 Spring Security 框架中找到適合實現該功能的切入點
想要找個框架的切入點必須對框架如何工作,源碼要熟悉,不然很難找到一個合適的切入點。有點見縫插針的意思,首先就需要找到一個適合「插針」的位置。
2.1 FilterSecurityInterceptor
FilterSecurityInterceptor 過濾器是 Spring Security 過濾器鏈條中的最後一個過濾器,它的任務是來最終決定一個請求是否可以被允許訪問。
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke 函數源碼:這個函數中做了調用下一個過濾器的操作,也就是這行代碼 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()) 。因為 FilterSecurityInterceptor 是Security 過濾器鏈條中的最後一個過濾器,再去調用下一個過濾器就是調用原始過濾器鏈條中的下一個過濾器了,這也就意味著請求是被允許訪問的。但是在調用下一個過濾器之前還有一行代碼 ,InterceptorStatusToken token = super.beforeInvocation(fi); 這一行代碼就會決定本次請求是否會被放行。
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// 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);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation 函數源碼:這個函數做的事情大致是對這次請求是禁止訪問還是允許訪問進行投票,如果投票都通過的話就允許訪問,如果有一票反對就會禁止訪問拋出異常結束後續處理流程。投票的依據就是通過這行代碼
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); 獲取到的。這行代碼也就是我實現功能的切入點。它先獲取了一個 SecurityMetadataSource 對象,然後通過這個對象獲取了投票的依據。 我的思路就是自定義 SecurityMetadataSource 類的子類,來替換掉 FilterSecurityInterceptor 中的 SecurityMetadataSource 實例。
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to "true"");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
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);
}
}
2.2 替換 FilterSecurityInterceptor 中的 SecurityMetadataSource 實例
我的目的是替換掉 FilterSecurityInterceptor 中的 SecurityMetadataSource 實例 , 而不是去替換掉原有的 FilterSecurityInterceptor , 如果要替換掉原有的 FilterSecurityInterceptor 那麼工作量就變大了,所以替換掉原有的 FilterSecurityInterceptor 並不是一個好的選擇。首先我需要找到 FilterSecurityInterceptor 對象是在什麼時候被實例化的。通過使用代碼搜索找到 FilterSecurityInterceptor 的實例化位置:org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer#createFilterSecurityInterceptor , 也是在這個函數中 SecurityMetadataSource 對象被設置。
private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,
FilterInvocationSecurityMetadataSource metadataSource,
AuthenticationManager authenticationManager) throws Exception {
FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
securityInterceptor.setSecurityMetadataSource(metadataSource);
securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));
securityInterceptor.setAuthenticationManager(authenticationManager);
securityInterceptor.afterPropertiesSet();
return securityInterceptor;
}
createFilterSecurityInterceptor 函數被調用的位置在 :org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer#configure 。這裡關鍵的一行代碼是 :securityInterceptor = postProcess(securityInterceptor);
@Override
public void configure(H http) throws Exception {
FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
if (metadataSource == null) {
return;
}
FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
http, metadataSource, http.getSharedObject(AuthenticationManager.class));
if (filterSecurityInterceptorOncePerRequest != null) {
securityInterceptor
.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
}
securityInterceptor = postProcess(securityInterceptor);
http.addFilter(securityInterceptor);
http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
org.springframework.security.config.annotation.SecurityConfigurerAdapter#postProcess 函數作用 :這個函數中使用了一個 objectPostProcessor 成員變數去調用了 postProcess 函數。 objectPostProcessor 成員變數默認是 org.springframework.security.config.annotation.SecurityConfigurerAdapter.CompositeObjectPostProcessor 的實現類。
protected <T> T postProcess(T object) {
return (T) this.objectPostProcessor.postProcess(object);
}
org.springframework.security.config.annotation.SecurityConfigurerAdapter.CompositeObjectPostProcessor#postProcess 函數源碼:這個類的 postProcess 函數中獲取到了多個 ObjectPostProcessor 對象,循環的進行調用。看到這裡我就找到解決我的問題的方法了,我提供一個 ObjectPostProcessor 實例對象添加到這個 ObjectPostProcessor 對象的列表中,然後在我自定義的 ObjectPostProcessor 對象中就可以獲取到原始的 FilterSecurityInterceptor 對象,然後對它進行操作,替換掉原有的 SecurityMetadataSource 對象。
public Object postProcess(Object object) {
for (ObjectPostProcessor opp : postProcessors) {
Class<?> oppClass = opp.getClass();
Class<?> oppType = GenericTypeResolver.resolveTypeArgument(oppClass,
ObjectPostProcessor.class);
if (oppType == null || oppType.isAssignableFrom(object.getClass())) {
object = opp.postProcess(object);
}
}
return object;
}
我進行替換 SecurityMetadataSource 操作的代碼 :
package org.hepeng.commons.spring.security.web;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* @author he peng
*/
public class CustomizeSecurityMetadataSourceObjectPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {
private SecurityConfigAttributeLoader securityConfigAttributeLoader;
public CustomizeSecurityMetadataSourceObjectPostProcessor(SecurityConfigAttributeLoader securityConfigAttributeLoader) {
this.securityConfigAttributeLoader = securityConfigAttributeLoader;
}
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
FilterSecurityInterceptor interceptor = object;
CustomizeConfigSourceFilterInvocationSecurityMetadataSource metadataSource =
new CustomizeConfigSourceFilterInvocationSecurityMetadataSource(
interceptor.obtainSecurityMetadataSource() , securityConfigAttributeLoader);
interceptor.setSecurityMetadataSource(metadataSource);
return (O) interceptor;
}
}
2.3 重寫自定義 SecurityMetadataSource 中的 org.springframework.security.access.SecurityMetadataSource#getAttributes 函數
org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer#createMetadataSource 函數在實例化 FilterSecurityInterceptor 對象之前被調用。Spring Security 默認提供了 ExpressionBasedFilterInvocationSecurityMetadataSource 的實例。我的思路是模仿這個類中 getAttributes 函數的實現。看了這個類的源碼後發現這個類中沒有重寫 getAttributes 函數,而是使用父類 DefaultFilterInvocationSecurityMetadataSource 的 getAttributes 函數。
@Override
final ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource(
H http) {
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = REGISTRY
.createRequestMap();
if (requestMap.isEmpty()) {
throw new IllegalStateException(
"At least one mapping is required (i.e. authorizeRequests().anyRequest().authenticated())");
}
return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap,
getExpressionHandler(http));
}
org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource#getAttributes 源碼:這就去操作了 requestMap 這個成員變數 , 這個成員變數的類型是 : Map<RequestMatcher, Collection<ConfigAttribute>> 。並且這個成員變數的值是在 ExpressionBasedFilterInvocationSecurityMetadataSource 對象的構造函數中進行傳遞給父類的。
public Collection<ConfigAttribute> getAttributes(Object object) {
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
ExpressionBasedFilterInvocationSecurityMetadataSource 源碼:在構造函數中就通過 processMap 函數完成了父類構造函數所需參數的創建。關鍵就是這個 org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource#processMap 函數。 我也需要調用這個 processMap 函數,但是這個函數是 private 的沒法直接調用, 所以只能是通過反射的方式調用。
public ExpressionBasedFilterInvocationSecurityMetadataSource(
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap,
SecurityExpressionHandler<FilterInvocation> expressionHandler) {
super(processMap(requestMap, expressionHandler.getExpressionParser()));
Assert.notNull(expressionHandler,
"A non-null SecurityExpressionHandler is required");
}
private static LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> processMap(
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap,
ExpressionParser parser) {
Assert.notNull(parser, "SecurityExpressionHandler returned a null parser object");
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestToExpressionAttributesMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(
requestMap);
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
RequestMatcher request = entry.getKey();
Assert.isTrue(entry.getValue().size() == 1,
"Expected a single expression attribute for " + request);
ArrayList<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(1);
String expression = entry.getValue().toArray(new ConfigAttribute[1])[0]
.getAttribute();
logger.debug("Adding web access control expression "" + expression + "", for "
+ request);
AbstractVariableEvaluationContextPostProcessor postProcessor = createPostProcessor(
request);
try {
attributes.add(new WebExpressionConfigAttribute(
parser.parseExpression(expression), postProcessor));
}
catch (ParseException e) {
throw new IllegalArgumentException(
"Failed to parse expression "" + expression + """);
}
requestToExpressionAttributesMap.put(request, attributes);
}
return requestToExpressionAttributesMap;
}
我自定義的 SecurityMetadataSource 源碼 :
package org.hepeng.commons.spring.security.web;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.joor.Reflect;
import org.springframework.expression.ExpressionParser;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author he peng
*/
public class CustomizeConfigSourceFilterInvocationSecurityMetadataSource extends DefaultFilterInvocationSecurityMetadataSource {
private static final Reflect REFLECT = Reflect.on(ExpressionBasedFilterInvocationSecurityMetadataSource.class);
private SecurityMetadataSource delegate;
private SecurityConfigAttributeLoader metadataSourceLoader;
private ExpressionParser expressionParser;
public CustomizeConfigSourceFilterInvocationSecurityMetadataSource(
SecurityMetadataSource delegate ,
SecurityConfigAttributeLoader metadataSourceLoader) {
super(new LinkedHashMap<>());
this.delegate = delegate;
this.metadataSourceLoader = metadataSourceLoader;
copyDelegateRequestMap();
}
private void copyDelegateRequestMap() {
Reflect reflect = Reflect.on(this);
reflect.set("requestMap" , getDelegateRequestMap());
}
private LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> getDelegateRequestMap() {
Reflect reflect = Reflect.on(this.delegate);
return reflect.field("requestMap").get();
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
Collection<ConfigAttribute> configAttributes = new ArrayList<>();
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap =
this.metadataSourceLoader.loadConfigAttribute(request);
if (MapUtils.isEmpty(requestMap)) {
configAttributes.addAll(this.delegate.getAttributes(object));
return configAttributes;
}
if (Objects.isNull(this.expressionParser)) {
SecurityExpressionHandler securityExpressionHandler = GlobalSecurityExpressionHandlerCacheObjectPostProcessor.getSecurityExpressionHandler();
if (Objects.isNull(securityExpressionHandler)) {
throw new NullPointerException(SecurityExpressionHandler.class.getName() + " is null");
}
this.expressionParser = securityExpressionHandler.getExpressionParser();
}
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> webExpressionRequestMap =
REFLECT.call("processMap" , requestMap , this.expressionParser).get();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : webExpressionRequestMap.entrySet()) {
if (entry.getKey().matches(request)) {
configAttributes.addAll(entry.getValue());
break;
}
}
if (CollectionUtils.isEmpty(configAttributes)) {
configAttributes.addAll(this.delegate.getAttributes(object));
}
return configAttributes;
}
}
為了實現解耦的目的我定義了一個 SecurityConfigAttributeLoader 介面 , 這個介面負責從任何指定的地方去讀取配置數據。關於該功能的代碼我都發布到了 maven 中央倉庫中 , 坐標是 :
<dependency>
<groupId>org.hepeng</groupId>
<artifactId>hp-java-commons</artifactId>
<version>1.1.3</version>
</dependency>
使用的時候只需要一行簡單的配置代碼 , 還有提供一個 SecurityConfigAttributeLoader 介面的實現,配置代碼 :org.hepeng.commons.spring.security.web.CustomizeSecurityConfigAttributeSourceConfigurer#public static <T extends ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry> T enable(T configurer) , 這個配置方式會從 Spring 的容器中去尋找一個 SecurityConfigAttributeLoader 實例對象。
{!-- PGC_COLUMN --}原創作者:大木老師故事的小黃花
https://my.oschina.net/j4love/blog/2988433


※js把屬性具有父子關係的json文件轉變成json樹
※徹底理解 Node.js 中的回調(Callback)函數
TAG:程序員小新人學習 |