當前位置:
首頁 > 知識 > 運行時動態的開關 Spring Security

運行時動態的開關 Spring Security

1. 為什麼要在運行時動態的開關 Spring Security?

考慮這樣一個場景,當我們構建了一整套微服務架構的系統後,公司某個內部的老系統也感受到了微服務架構的好處,包括實時監控,限流,熔斷,高可用的機制等等,老系統的開發人員也希望能減少自己的一些工作量,所以他們系統將老系統加入到我們的微服務架構體系中來。這樣就產生了一些適配,兼容性問題,如果讓老系統來完全適配已經構建好的微服務架構體系那麼老系統改動的代價就比較大,包括技術的升級,開發人員的學習成本提高,測試問題,還有老系統還有一些不斷的新需求要開發。比較理想的解決方案是對老系統的改動越小越好,最好能做到無縫集成,已經構建好的微服務架構來為老系統的集成提供支持。比如說老系統原本有自己的認證,授權控制,使用了 Spring Security ,在微服務架構中我們將認證,授權的工作統一放在了 API 網關層去處理。這樣就和老系統的集成產生了衝突。於是我就需要讓 API 網關路由到老系統上的請求不經過老系統自身的認證、授權流程,也可以正常訪問。同時也不能破壞當不通過 API 網關訪問時老系統的認證、授權流程也要能正常工作。所以這是我要達到的目的。

2. Spring Security 在 Web 項目中是如何工作的

這是我在網上找的一張圖,目的就是為了大概說明問題。 Spring Web Security 的核心功能都是在這一條過濾器鏈上完成的。具體可以參考這個類 :

org.springframework.security.config.annotation.web.builders.FilterComparator , 這個類中定義了所有的 Spring Security 的過濾器以及他們的順序。

搞清楚這一點,我就有一個想法,既然我想要關閉 Spring Security 不讓他起作用 ,那我不讓請求經過這些過濾器不就可以了么。

運行時動態的開關 Spring Security

FilterComparator 源碼 :

private static final int STEP = 100;
private Map<String, Integer> filterToOrder = new HashMap<String, Integer>();
FilterComparator() {
int order = 100;
put(ChannelProcessingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(WebAsyncManagerIntegrationFilter.class, order);
order += STEP;
put(SecurityContextPersistenceFilter.class, order);
order += STEP;
put(HeaderWriterFilter.class, order);
order += STEP;
put(CorsFilter.class, order);
order += STEP;
put(CsrfFilter.class, order);
order += STEP;
put(LogoutFilter.class, order);
order += STEP;
put(X509AuthenticationFilter.class, order);
order += STEP;
put(AbstractPreAuthenticatedProcessingFilter.class, order);
order += STEP;
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
order);
order += STEP;
put(UsernamePasswordAuthenticationFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
filterToOrder.put(
"org.springframework.security.openid.OpenIDAuthenticationFilter", order);
order += STEP;
put(DefaultLoginPageGeneratingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(DigestAuthenticationFilter.class, order);
order += STEP;
put(BasicAuthenticationFilter.class, order);
order += STEP;
put(RequestCacheAwareFilter.class, order);
order += STEP;
put(SecurityContextHolderAwareRequestFilter.class, order);
order += STEP;
put(JaasApiIntegrationFilter.class, order);
order += STEP;
put(RememberMeAuthenticationFilter.class, order);
order += STEP;
put(AnonymousAuthenticationFilter.class, order);
order += STEP;
put(SessionManagementFilter.class, order);
order += STEP;
put(ExceptionTranslationFilter.class, order);
order += STEP;
put(FilterSecurityInterceptor.class, order);
order += STEP;
put(SwitchUserFilter.class, order);
}

3. Spring Security 的過濾器鏈是如何工作的

3.1 Spring Security 過濾器鏈是什麼 ?

通過 debug 調試 可以發現在 Spring Security 提供的過濾器中使用的 FilterChain 的實際類型是這個類 : org.springframework.security.web.FilterChainProxy.VirtualFilterChain 。它實現了 FilterChain 介面。

3.2 Spring Security 過濾器鏈初始化

通過搜索可以找到過濾器鏈條是在這個函數中進行初始化的 : org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild 源碼:

@Override
protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
"At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("

"
+ "********************************************************************
"
+ "********** Security debugging is enabled. *************
"
+ "********** This may include sensitive information. *************
"
+ "********** Do not use in a production system! *************
"
+ "********************************************************************

");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result;
}

org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain 源碼:FilterChainProxy 本身也是一個過濾器這個過濾器會被註冊到過濾器鏈上。然後這個過濾器內部封裝了 Spring Security 的過濾器鏈條。

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

3.3 Spring Security 過濾器鏈的工作過程

org.springframework.security.web.FilterChainProxy#doFilter 源碼 :

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}

org.springframework.security.web.FilterChainProxy#doFilterInternal 源碼 : 這個函數是 Spring Security 過濾器鏈條的執行入口。每次請求都會 new 一個 VirtualFilterChain 的實例對象,然後調用該對象的 doFilter 函數,於是請求就進入到 Spring Security 的過濾器鏈處理中。

private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}

org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter 源碼 :這裡就是去挨個調用 Spring Security 的過濾器的過程 ,重點需要關注的是 originalChain (原始的過濾器鏈條也就是 servlet 容器的) , currentPosition (spring security 過濾器鏈當前執行到的位置) , size (spring security 過濾器鏈中過濾器的個數) 。 當 currentPosition == size 的時候也就意味著 spring security 的過濾器鏈條執行完了,於是就該使用原始的 originalChain 繼續去調用 servlet 容器中註冊的過濾器了。

public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: ""
+ nextFilter.getClass().getSimpleName() + """);
}
nextFilter.doFilter(request, response, this);
}
}
}

3.4 運行時跳過 Spring Security 過濾器鏈的思路

了解上面講的 spring security 過濾器鏈的執行過程後如何跳過spring security 的過濾器鏈就顯而易見了 , 只需要控制 org.springframework.security.web.FilterChainProxy.Virt ualFilterChain 對象中的 currentPosition == size 就可以了。但是 org.springframework.security.web.FilterChainProxy.VirtualFilterChain 這個類是內部私有的靜態成員類。 Spring Security 的目的就是為了封裝它將它隱藏起來,想想也可以理解畢竟這是它之所以能實現功能的核心,肯定不希望被亂動。但是沒辦法為了實現我的需求,我還是要亂動它,想要改變它的話就只有通過反射的方式才能實現。

3.4.1 關於反射的小技巧

Class.forName("org.springframework.security.web.FilterChainProxy.VirtualFilterChain"); 使用這行代碼的時候是不能成功獲取到 VirtualFilterChain 類的 Class 對象的。因為 Java 中內部類在編譯成 .class 文件後名稱是這樣的 FilterChainProxy$VirtualFilterChain 。想要成功獲取到這個內部類的 Class 對象的話需要這樣寫 Class.forName("org.springframework.security.web.FilterChainProxy$VirtualFilterChain");

3.5 運行時跳過 Spring Security 過濾器鏈的實現方式

首先我自定義了一個過濾器並且把它加入到 Spring Security 過濾器鏈條中的最前面,因為我的目的是完全的「關閉」掉spring security 的過濾器鏈。示例代碼 :通過反射的方式改變 currentPosition 的值即可。

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (! isSkipOver(request)) {
filterChain.doFilter(request , response);
return;
}
Class<? extends FilterChain> filterChainClass = filterChain.getClass();
try {
Class<?> virtualFilterChainClass =
Class.forName("org.springframework.security.web.FilterChainProxy$VirtualFilterChain");
if (virtualFilterChainClass.isAssignableFrom(filterChainClass)) {
Reflect reflect = Reflect.on(filterChain);
Object size = reflect.field("size").get();
reflect.set("currentPosition" , size);
}
} catch (Throwable t) {
throw new ApplicationRuntimeException(t);
}
filterChain.doFilter(request , response);
}

4. 廣告

如果你也面臨到我所說的類似問題,需要編寫一些代碼來解決你的問題的話,那麼上面這些代碼已經不需要你再去花時間編寫了,我已經寫好了。你只需要通過 maven :

<dependency>

<groupId>org.hepeng</groupId>

<artifactId>hp-java-commons</artifactId>

<version>1.1.3</version>

</dependency>

或者是 gradle : implementation "org.hepeng:hp-java-commons:1.1.3"

再自定義一個 Filter 並且繼承 org.hepeng.commons.spring.security.web.filter.SkipOverSpringSecurityFilterChainFilter 即可。它已經經過了我的多次測試可以正常工作,如果你發現任何 bug 可以向我反饋,我會儘快修復。

作者:OSC首席混子

原文:https://my.oschina.net/j4love/blog/2989947

運行時動態的開關 Spring Security

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

Docker的一些基本操作
3道趣味Python題,非常適合菜鳥練手

TAG:程序員小新人學習 |