Spring Security原理學習--核心過濾器Filter(二)
在上一篇部落格Spring Security原理學習--簡介與示例(一)中已經簡單介紹了Spring Security相關的東西, 這篇部落格我們介紹一下Spring Security相關的實現機制 。首先Spring Security的認證功能是依賴Filter實現的,當然在認證功能基礎上還提供了一些安全的驗證等都是依賴Filter來實現完成的,如下截圖Spring Security提供了13個功能Filter,並且是按照如下順序依次執行的。當然配合Spring web的Filter注入實現,Spring Security提供了另外一個Filter的實現類FilterChainProxy,其對外包裝了以下13個Filter,其實13個Filter是在介面SecurityFilterChain的實現類DefaultSecurityFilterChain中通過List儲存的。
接下來我們依次介紹一下這個13個Filter。
1、WebAsyncManagerIntegrationFilter
public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter { private static final Object CALLABLE_INTERCEPTOR_KEY = new Object(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY); if (securityProcessingInterceptor == null) { asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, new SecurityContextCallableProcessingInterceptor()); } filterChain.doFilter(request, response); } }
- 根據請求封裝獲取WebAsyncManager
- 從WebAsyncManager獲取/註冊SecurityContextCallableProcessingInterceptor
2、SecurityContextPersistenceFilter
兩個主要職責:請求來臨時,建立SecurityContext安全上下文資訊,請求結束時清空SecurityContextHolder。
public class SecurityContextPersistenceFilter extends GenericFilterBean { static final String FILTER_APPLIED = "__spring_security_scpf_applied"; private SecurityContextRepository repo; private boolean forceEagerSessionCreation = false; public SecurityContextPersistenceFilter() { this(new HttpSessionSecurityContextRepository()); } public SecurityContextPersistenceFilter(SecurityContextRepository repo) { this.repo = repo; } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //判斷是否已經處理過 if (request.getAttribute(FILTER_APPLIED) != null) { // ensure that filter is only applied once per request chain.doFilter(request, response); return; } final boolean debug = logger.isDebugEnabled(); request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (forceEagerSessionCreation) { HttpSession session = request.getSession(); if (debug && session.isNew()) { logger.debug("Eagerly created session: " + session.getId()); } } HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); //獲取SecurityContext SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { //結束後清理SecurityContext SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); // Crucial removal of SecurityContextHolder contents - do this before anything // else. SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } } public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) { this.forceEagerSessionCreation = forceEagerSessionCreation; } }
3、HeaderWriterFilter
用來給http響應新增一些Header,比如X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options.
public class HeaderWriterFilter extends OncePerRequestFilter {
/**
* Collection of {@link HeaderWriter} instances to write out the headers to the
* response.
*/
private final List<HeaderWriter> headerWriters;
/**
* Creates a new instance.
*
* @param headerWriters the {@link HeaderWriter} instances to write out headers to the
* {@link HttpServletResponse}.
*/
public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
this.headerWriters = headerWriters;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
response, this.headerWriters);
try {
filterChain.doFilter(request, headerWriterResponse);
}
finally {
headerWriterResponse.writeHeaders();
}
}
static class HeaderWriterResponse extends OnCommittedResponseWrapper {
private final HttpServletRequest request;
private final List<HeaderWriter> headerWriters;
HeaderWriterResponse(HttpServletRequest request, HttpServletResponse response,
List<HeaderWriter> headerWriters) {
super(response);
this.request = request;
this.headerWriters = headerWriters;
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.util.OnCommittedResponseWrapper#
* onResponseCommitted()
*/
@Override
protected void onResponseCommitted() {
writeHeaders();
this.disableOnResponseCommitted();
}
protected void writeHeaders() {
if (isDisableOnResponseCommitted()) {
return;
}
for (HeaderWriter headerWriter : this.headerWriters) {
headerWriter.writeHeaders(this.request, getHttpResponse());
}
}
private HttpServletResponse getHttpResponse() {
return (HttpServletResponse) getResponse();
}
}
}
4、LogoutFilter
退出攔截器,退出的簡單操作就是刪除Session,根據Spring Security初始化配置的退出地址來匹配請求。
public class LogoutFilter extends GenericFilterBean {
// ~ Instance fields
// ================================================================================================
private RequestMatcher logoutRequestMatcher;
private final List<LogoutHandler> handlers;
private final LogoutSuccessHandler logoutSuccessHandler;
// ~ Constructors
// ===================================================================================================
/**
* Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
* target destination after logging out. The list of <tt>LogoutHandler</tt>s are
* intended to perform the actual logout functionality (such as clearing the security
* context, invalidating the session, etc.).
*/
public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler,
LogoutHandler... handlers) {
Assert.notEmpty(handlers, "LogoutHandlers are required");
this.handlers = Arrays.asList(handlers);
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
Assert.notEmpty(handlers, "LogoutHandlers are required");
this.handlers = Arrays.asList(handlers);
Assert.isTrue(
!StringUtils.hasLength(logoutSuccessUrl)
|| UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
logoutSuccessUrl + " isn't a valid redirect URL");
SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
if (StringUtils.hasText(logoutSuccessUrl)) {
urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
}
logoutSuccessHandler = urlLogoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
// ~ Methods
// ========================================================================================================
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//判斷是不是退出請求
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
for (LogoutHandler handler : handlers) {
//在logoutHandler中進行刪除session操作
handler.logout(request, response, auth);
}
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
/**
* Allow subclasses to modify when a logout should take place.
*
* @param request the request
* @param response the response
*
* @return <code>true</code> if logout should occur, <code>false</code> otherwise
*/
protected boolean requiresLogout(HttpServletRequest request,
HttpServletResponse response) {
return logoutRequestMatcher.matches(request);
}
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
this.logoutRequestMatcher = logoutRequestMatcher;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
}
}
當判斷請求是退出時,會呼叫LogoutHandler的logout刪除session,具體實現在SecurityContextLogoutHandler的logout方法中。
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
//設定session無效
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Invalidating session: " + session.getId());
session.invalidate();
}
}
if (clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
}
//清理資訊
SecurityContextHolder.clearContext();
}
5、UsernamePasswordAuthenticationFilter
使用者名稱和密碼校驗Filter,是特別重要的一個Filter,我們會在使用者登入驗證部落格中專門進行分析學習一下,當然這個Filter只會對配置的登入請求/login進行業務處理,其他請求不做任何業務處理,其處理邏輯在父類AbstractAuthenticationProcessingFilter中。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//首先判斷是不是登入請求,不是登入請求則直接跳過
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
//是登入請求進行校驗工作
//省略部分程式碼
authResult = attemptAuthentication(request, response);
//省略部分程式碼
}
6、DefaultLoginPageGeneratingFilter
當是登入地址請求,登入失敗請求或者登出請求則跳轉到登入頁面。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
//登入請求,登入錯誤請求或者登出請求則返回登入頁面
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.length());
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
7、BasicAuthenticationFilter
處理BASIC authentication認證方式,簡單理解和UsernamePasswordAuthenticationFilter類似,不過UsernamePasswordAuthenticationFilter是通過表單提交的,而Authorization認證方式是將使用者名稱密碼資料新增到請求header中罷了。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
//請求頭中獲取Authorization
String header = request.getHeader("Authorization");
//如果不存在或者不是以Basic開頭直接跳過處理
if (header == null || !header.startsWith("Basic ")) {
chain.doFilter(request, response);
return;
}
//從header中獲取相關使用者名稱密碼資料進行驗證
try {
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String username = tokens[0];
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
8、RequestCacheAwareFilter
內部維護了一個RequestCache,用於快取request請求
public class RequestCacheAwareFilter extends GenericFilterBean {
private RequestCache requestCache;
public RequestCacheAwareFilter() {
this(new HttpSessionRequestCache());
}
public RequestCacheAwareFilter(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
}
9、SecurityContextHolderAwareRequestFilter
此過濾器對ServletRequest進行了一次包裝,使得request具有更加豐富的API
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
(HttpServletResponse) res), res);
}
10、AnonymousAuthenticationFilter
匿名身份過濾器,這個過濾器個人認為很重要,需要將它與UsernamePasswordAuthenticationFilter 放在一起比較理解,spring security為了相容未登入的訪問,也走了一套認證流程,只不過是一個匿名的身份。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//建立一個匿名身份
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
chain.doFilter(req, res);
}
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
11、SessionManagementFilter
和session相關的過濾器,內部維護了一個SessionAuthenticationStrategy,兩者組合使用,常用來防止session-fixation protection attack,以及限制同一使用者開啟多個會話的數量
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (!securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null && !trustResolver.isAnonymous(authentication)) {
// The user has been authenticated during the current request, so call the
// session strategy
try {
sessionAuthenticationStrategy.onAuthentication(authentication,
request, response);
}
catch (SessionAuthenticationException e) {
// The session strategy can reject the authentication
logger.debug(
"SessionAuthenticationStrategy rejected the authentication object",
e);
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, e);
return;
}
// Eagerly save the security context to make it available for any possible
// re-entrant
// requests which may occur before the current request completes.
// SEC-1396.
securityContextRepository.saveContext(SecurityContextHolder.getContext(),
request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
if (request.getRequestedSessionId() != null
&& !request.isRequestedSessionIdValid()) {
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " is invalid.");
}
if (invalidSessionStrategy != null) {
invalidSessionStrategy
.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
12、ExceptionTranslationFilter
ExceptionTranslationFilter異常轉換過濾器位於整個springSecurityFilterChain的後方,用來轉換整個鏈路中出現的異常,將其轉化,顧名思義,轉化以意味本身並不處理。一般其只處理兩大類異常:AccessDeniedException訪問異常和AuthenticationException認證異常。這個過濾器非常重要,因為它將Java中的異常和HTTP的響應連線在了一起,這樣在處理異常時,我們不用考慮密碼錯誤該跳到什麼頁面,賬號鎖定該如何,只需要關注自己的業務邏輯,丟擲相應的異常便可。如果該過濾器檢測到AuthenticationException,則將會交給內部的AuthenticationEntryPoint去處理,如果檢測到AccessDeniedException,需要先判斷當前使用者是不是匿名使用者,如果是匿名訪問,則和前面一樣執行AuthenticationEntryPoint,否則會委託給AccessDeniedHandler去處理,而AccessDeniedHandler的預設實現,是AccessDeniedHandlerImpl。所以ExceptionTranslationFilter內部的AuthenticationEntryPoint是至關重要的,顧名思義:認證的入口點。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
//捕捉一些異常處理,身份校驗判斷錯誤會丟擲異常,由這裡處理
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
13、FilterSecurityInterceptor
個人理解任務這個攔截器是Spring Security中最重要的,首先這個攔截器就是進行角色相關的處理,也是登入結果是否成功最後一個判斷的攔截器,針對不需要認證和需要認證的請求會進行不同的驗證處理,同樣在登入部落格中會對其做的功能進行詳細的說明。
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(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);
}
}