一、整理了解下Spring Security 的工作原理
如上圖所示,spring security 的主要工作就是在原有的網絡請求的過濾器鏈(ApplicationFilterChain)中額外添加一條過濾器鏈(FilterChainProxy),主要用於用戶認證與授權。
請求進入web容器經一些容器自身的基礎加工後,進入到servlet的濾器鏈中,spring security 使用 DelegatingFilterProxy 這個filter,將 targetBeanName 設置爲 "springSecurityFilterChain" ,也就是security生成的 FilterChainProxy 這個類的bean的名字,使 servlet 過濾器鏈 指向了 spring security的過濾器鏈 ,同時也進入到spring 容器當中。經過一系列的認證工作,若認證成功後,會生成一個Authentication類型的對象(可以記錄當前登錄人的用戶信息,權限信息),放入到SecurityContext中(默認以ThreadLocal的方式保存),然後返回servlet 過濾器鏈中,向控制層前行。
security的過濾器鏈會根據你所配置需要啓停的功能增減,同時你也可以創建自己的過濾器添加進這個過濾器鏈中。
二、FilterChainProxy源碼分析
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//此處是對請求是否第一次經過這個過濾器的判斷,在spring security中的過濾器中這種用法是相當常見的
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
//清除security上下文中的保存的Authentication對象
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//對request進行一層包裝,同時校驗了一下請求的方式是否允許,以及請求地址中是否有非法字符,若不通過拋出異常
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
//對response進行了一層包裝,若在response中添加header或cookie時,會對header和cookie的一些屬性進行校驗
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
//獲取spring security中第一個匹配請求地址的過濾器列表
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;
}
//將過濾器列表和servlet過濾器鏈包裝成一個新的過濾器鏈 VirtualFilterChain 是FilterChainProxy的內部類
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain;
private final List<Filter> additionalFilters;
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0;
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
//security 過濾器執行結束後,繼續執行servlet過濾器鏈
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() + "'");
}
//傳入this,nextFilter中的會繼續調用VirtualFilterChain.doFilter()直至所有過濾器執行結束或拋出異常
nextFilter.doFilter(request, response, this);
}
}
}
FilterChainProxy過濾器通過內部的FilterChain,將請求引入到了security的過濾器鏈中,上述對其主要的方法進行了簡單描述。
三、基於SpringBoot,security的加載過程
springboot 啓動時 有關spring security 主要加載的類有SecurityAutoConfiguration、SecurityRequestMatcherProviderAutoConfiguration、UserDetailsServiceAutoConfiguration、SecurityFilterAutoConfiguration , 其中需要講一下SecurityAutoConfiguration 和SecurityFilterAutoConfiguration。
1、 SecurityAutoConfiguration 引入了SpringBootWebSecurityConfiguration、WebSecurityEnablerConfiguration、SecurityDataConfiguration 這三個配置類:
SpringBootWebSecurityConfiguration 當你沒有主動配置security時,這個類的作用會生效,生一套默認的配置
WebSecurityEnablerConfiguration 的作用全在@EnableWebSecurity 這個註解上,使Security功能生效,並且承擔了其中最主要的初始化工作。
@EnableWebSecurity 引入了 WebSecurityConfiguration類 ,在這個類中讀取了所有的security配置(可以配置多個spring security 過濾器鏈),並根據這些配置生成一個name爲“springSecurityFilterChain” 的bean,裏邊進行的動作相當多,有興趣的同學可以研讀一下這一塊源碼。
SecurityDataConfiguration 屬於spring security 的一個擴展配置,此處省略。
2、SecurityFilterAutoConfiguration 的作用只有一個,就是創建一個DelegatingFilterProxyRegistrationBean,將spring security 的過濾器鏈springSecurityFilterChain 與servlet過濾器相連。
四、注意事項
1、當手動添加過濾器到spring security中時,如果這個過濾器添加到spring 容器中,spring 自動會將此過濾器添加到servlet過濾器鏈中,此時,兩條過濾器鏈都會有這個過濾器,則會執行兩次。
解決辦法:
讓這個過濾器繼承OncePerRequestFilter ,這個過濾器內部保證只執行一次。
或者不要將這個過濾器添加到spring容器中。
2、在使用spring security 自帶的csrf防護功能時,在前端併發請求會導致認證不通過,原因是前端同時需要發起多個請求時,這幾個請求的發送總是會有一些先後順序,當第n(n>1)個請求準備發起時,獲取到的cookie中的csrfToken爲token1, 此時第一個請求的已響應,cookie中的csrfToken隨之替換成第一個請求中返回的cookie中的csrfToken2,這時第n個請求發出,header中攜帶的是token1,cookie中放的是token2,兩者不相等,請求拒絕。 所以這個功能要視具體情況而用。