Spring Security 實現 antMatchers 配置路徑的動態獲取
1. 為什麼要實現動態的獲取 antMatchers 配置的資料
這兩天由於公司專案的需求,對 spring security 的應用過程中需要實現動態的獲取 antMatchers ,permitAll , hasAnyRole , hasIpAddress 等這些原本通過硬編碼的方式配置的資料。為了讓每一個業務服務不用再去處理許可權驗證等這些和業務無關的邏輯,而是隻專注於它所負責的業務,就要將認證、授權統一的放在 API 閘道器層去處理。但是每個不同的業務服務有的介面需要認證後才能訪問,有的介面是不需要認證就可以訪問的,有的介面可能是需要某些許可權、角色才可以訪問。這樣依賴 API 閘道器就必須知道並且能夠區分出來每個業務服務的介面哪些是需要認證後才可以訪問的,那些介面是不需要經過認證就可以訪問的。 為了實現這個功能 spring security 提供的 antMatchers 函式硬編碼的方式就不適用了。而是應該提供一個管理端,每個業務服務把他們這些個性化的介面通過管理端去進行配置,統一的儲存起來,spring security 在獲取這些資料的時候從統一的儲存中來獲取這些資料。基於這個需求前提我來考慮如何實現這個功能。配套視訊講解地址 :
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 例項物件。