@[TOC](spring security OAuth實踐(未完待續))
參考文章
核心filter
spring security維護了一個FilterChainProxy
,這個類會依次調用spring security過濾器鏈,默認的過濾器鏈是11個,加上自定義的(MyusernamePasswordAuthentication
是自定義過濾器,用於代替默認的UsernamePasswordAuthentication
)一個是十二個,如圖,調用順序從0-12
SecurityContextPersistenceFilter
整個Spring Security 過濾器鏈的開端,它有兩個作用:一是當請求到來時,檢查Session中是否存在SecurityContext
,如果不存在,就創建一個新的SecurityContext
。二是請求結束時將SecurityContext
放入 session
中,並清空 SecurityContextHolder
。
UsernamePasswordAuthenticationFilter
繼承自抽象類 AbstractAuthenticationProcessingFilter
,當進行表單登錄時,該Filter將用戶名和密碼封裝成一個 UsernamePasswordAuthentication
進行驗證。
改filter主要校驗表單參數,之後封裝成一個 UsernamePasswordAuthentication
,如果需要修改spring security自動生成的表單元素(如添加一個驗證碼參數),需要自定義一個UsernamePasswordAuthenticationFilter
,在過濾器配置自定義filter以替換UsernamePasswordAuthenticationFilter
實現表單校驗構造UsernamePasswordAuthentication
AnonymousAuthenticationFilter
匿名身份過濾器,當前面的Filter認證後依然沒有用戶信息時,該Filter會生成一個匿名身份——AnonymousAuthenticationToken
。一般的作用是用於匿名登錄。
ExceptionTranslationFilter
異常轉換過濾器,用於處理 FilterSecurityInterceptor
拋出的異常。
過濾器鏈經過此filter時會進行try…catch…,如果後面的filter拋出異常,會在此處捕獲並處理(如後面的FilterSecurityInterceptor
投票後拋出SpringSecurityException
)
FilterSecurityInterceptor
過濾器鏈最後的關卡,從 SecurityContextHolder
中獲取 Authentication
,比對用戶擁有的權限和所訪問資源需要的權限。
認證過程
認證過程之後再補充
如果需要自定義認證過程需要自定義LoginAuthenticationProvide
實現AuthenticationProvider
接口(AuthenticationProvider
默認的實現是DaoAuthenticationProvider
)
重寫authenticate
和supports
方法
authenticate
方法參考DaoAuthenticationProvider
類的authenticate
方法,認證成功返回一個UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities())
對象,認證失敗拋出AuthenticationException
異常,AuthenticationException
異常是一個spring security的抽象異常,它有很多子類實現,其中之一就是UsernameNotFoundException
和BadCredentialsException
讀取保存在session中的認證信息
SecurityContextPersistenceFilter
當我們填寫表單完畢後,點擊登錄按鈕,請求先經過 SecurityContextPersistenceFilter
過濾器,在前面就曾提到,該Filter有兩個作用,其中之一就是在請求到來時,創建 SecurityContext
安全上下文,我們來看看它內部是如何做的,部分源碼如下:
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
/**安全上下文存儲的倉庫*/
private SecurityContextRepository repo;
private boolean forceEagerSessionCreation = false;
/**使用HttpSession來存儲 SecurityContext*/
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;
/**如果是第一次請求,request中肯定沒有FILTER_APPLIED屬性*/
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 設置 FILTER_APPLIED 屬性爲 true,
* 這樣同一個請求再次訪問時,就直接進入後續Filter的操作
*/
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
/**
* 封裝 requset 和 response
*/
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
/**
* 從存儲安全上下文的倉庫中載入 SecurityContext 安全上下文,
* 其內部是從 Session中獲取上下文信息
*/
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
/**
* 安全上下文信息設置到 SecurityContextHolder 中,
* 以便在同一個線程中,後續訪問 SecurityContextHolder
* 能獲取到 SecuritContext*/
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
/**
* 請求結束後,清空安全上下文信息
*/
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
/**
* 將安全上下文信息存儲到 Session中,相當於登錄態的維護
*/
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;
}
}
請求到來時,利用HttpSessionSecurityContextRepository
讀取安全上下文。我們這裏是第一次請求,讀取的安全上下文中是沒有 Authentication身份信息的,將安全上下文設置到 SecurityContextHolder
之後,進入下一個過濾器。
請求結束時,同樣利用HttpSessionSecurityContextRepository
該存儲安全上下文的倉庫將認證後的SecurityContext
放入 session
中,這也是登錄態維護的關鍵,具體調用的是 SecurityContextRepository
的saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
HttpSessionSecurityContextRepository
HttpSessionSecurityContextRepository
的save()
方法
@Override
protected void saveContext(SecurityContext context) {
/**從上下文中獲取到認證信息*/
final Authentication authentication = context.getAuthentication();
HttpSession httpSession = request.getSession(false);
// See SEC-776
/**認證信息爲空或者是匿名用戶*/
if (authentication == null || trustResolver.isAnonymous(authentication)) {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
}
if (httpSession != null && authBeforeExecution != null) {
// SEC-1587 A non-anonymous context may still be in the session
// SEC-1735 remove if the contextBeforeExecution was not anonymous
httpSession.removeAttribute(springSecurityContextKey);
}
return;
}
if (httpSession == null) {
httpSession = createNewSessionIfAllowed(context);
}
// If HttpSession exists, store current SecurityContext but only if it has
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
if (httpSession != null) {
// We may have a new session, so check also whether the context attribute
// is set SEC-1561
if (contextChanged(context)
|| httpSession.getAttribute(springSecurityContextKey) == null) {
/**設置上下文信息到session的attribute中,保存了認證狀態*/
httpSession.setAttribute(springSecurityContextKey, context);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext '" + context
+ "' stored to HttpSession: '" + httpSession);
}
}
}
}
SecurityContext
SecurityContext
是一個接口,有兩個方法
- 獲取認證信息和設置認證信息
UsernamePasswordAuthenticationFilter
的父類是 AbstractAuthenticationProcessingFilter
,首先進入父類的 doFilter方法,部分源碼如下:
該doFilter方法中一個核心就是調用子類 UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法,該方法進入真正的認證過程,並返回認證後的 Authentication
,該方法的源碼如下:
該方法中有一個關鍵點就是 this.getAuthenticationManager().authenticate(authRequest)
,調用內部的AuthenticationManager
去認證,在之前的文章就介紹過AuthenticationManager
,它是身份認證的核心接口,它的實現類是 ProviderManager
,而 ProviderManager
又將請求委託給一個 AuthenticationProvider
列表,列表中的每一個 AuthenticationProvider
將會被依次查詢是否需要通過其進行驗證,每個 provider的驗證結果只有兩個情況:拋出一個異常或者完全填充一個 Authentication
對象的所有屬性
下面來分析一個關鍵的 AuthenticationProvider
,它就是 DaoAuthenticationProvider,它是框架最早的provider,也是最最常用的 provider。大多數情況下我們會依靠它來進行身份認證,它的父類是 AbstractUserDetailsAuthenticationProvider
,認證過程首先會調用父類的 authenticate
方法,核心源碼如下:
從上面一大串源碼中,提取幾個關鍵的方法:
retrieveUser(…): 調用子類 DaoAuthenticationProvider
的 retrieveUser()
方法獲取 UserDetails
preAuthenticationChecks.check(user)
: 對從上面獲取的UserDetails進行預檢查,即判斷用戶是否鎖定,是否可用以及用戶是否過期
additionalAuthenticationChecks(user,authentication)
: 對UserDetails附加的檢查,對傳入的Authentication與獲取的UserDetails進行密碼匹配
postAuthenticationChecks.check(user)
: 對UserDetails進行後檢查,即檢查UserDetails的密碼是否過期
createSuccessAuthentication(principalToReturn, authentication, user)
: 上面所有檢查成功後,利用傳入的Authentication 和獲取的UserDetails生成一個成功驗證的Authentication
如何自定義登錄security
構造一個org.springframework.security.core.userdetails.User( user.getAccountID() + "|" + user.getUserName(), user.getPassword(), authorities);
對象
構造一個UsernamePasswordAuthenticationToken
通過authenticate(usernamePasswordAuthenticationToken)
進行認證,認證之後保存在上下文
SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(authentication);
調用結束SecurityContextPersistenceFilter
會將上下文保存在session中,這樣就將自定義的登錄與security的認證兼容了(可以不使用security的認證了)
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, password, userDetails.getAuthorities());
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authentication);
還有一種不經過security登錄認證兼容已有登錄
在 WebSecurityConfigurerAdapter
的子類配置中,配置一個自定義filter,插入在
SecurityContextPersistenceFilter
前,在這個filter中兼容已有登錄狀態,將認證信息放在session的attribute中,這樣執行到SecurityContextPersistenceFilter
時也可以從session中加載到認證信息
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
username, "", authorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, "", userDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(usernamePasswordAuthenticationToken);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
http.addFilterBefore(accessTokenFilter, StopSecurityDefaultEndpointFilter.class);
自定義退出登錄
退出登錄源碼參考
security的默認退出登錄邏輯在LogoutFilter
維護了一個LogoutSuccessHandler
,這個類決定了退出登錄成功後的重定向,默認是SimpleUrlLogoutSuccessHandler
,handler默認是CompositeLogoutHandler
,如果想要修改重定向的方式,可以自定義一個LogoutSuccessHandler配置 在 SecurityConfig中配置,http.logout() .logoutSuccessHandler(logoutSuccessHandler)
自定義未認證處理
實現AccessDeniedHandler
接口,重寫handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
方法自定義返回值即可
自定義TokenEndPoint異常處理
ProviderManager
ProviderManager
實現WebResponseExceptionTranslator
接口,重寫translate(Exception e)
方法,
獲取公鑰端點和校驗token端點權限配置
security.tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()")
這兩個端點是跨域配置登錄後訪問的,如果使用的是自定義登錄的方式,配置security.addTokenEndpointAuthenticationFilter(MyEndpointFilter)
MyEndpointFilter是自己實現的filter,在此filter中兼容自己的登錄方式,把認證信息存儲在session的attribute中
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
username, "", authorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, "", userDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(usernamePasswordAuthenticationToken);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
這樣,這兩個端點檢查認證的時候就可以識別到用戶已經登錄了
如果需要修改未登錄範湖IDE錯誤,可以實現AuthenticationEntryPoint
接口,實現commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)
方法,配置security.authenticationEntryPoint(MyauthenticationEntryPoint)
就可以了