Spring 源碼分析(五)Sercurity
sschrodienger
2019/03/04
Filter 代理組件分析
DelegatingFilterProxy
DelegatingFilterProxy
是標準 servlet
過濾器的一個代理類推沸,它可以代理 Spring 容器中實現(xiàn)了 Filter
接口的 Bean,以方便該過濾器獲得 Spring 依賴注入以及生命周期的支持夺刑。
DelegatingFilterProxy
維護了一個代理類 private volatile Filter delegate
,delegate
是最重要的執(zhí)行過濾器。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
可以看到墓臭,當 DelegatingFilterProxy
注冊到 tomcat 時,doFilter()
方法主要是調(diào)用 invokeDelegate()
方法 來執(zhí)行代理的 doFilter()
方法妖谴。
FilterChainProxy
在 DelegatingFilterProxy
中窿锉,delegate
變量的實際類型是 FilterChainProxy
。關(guān)鍵代碼如下:
public class FilterChainProxy extends GenericFilterBean {
// ~ Instance fields
// ================================================================================================
private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
".APPLIED");
private List<SecurityFilterChain> filterChains;
private HttpFirewall firewall = new StrictHttpFirewall();
// ~ Methods
// ========================================================================================================
@Override
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);
}
}
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);
//如果得到的過濾器的數(shù)量為零窖维,則直接跳過
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);
}
/**
* Returns the first filter chain matching the supplied URL.
*
* @param request the request to match
* @return an ordered array of Filters defining the filter chain
*/
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
/**
* Convenience method, mainly for testing.
*
* @param url the URL
* @return matching filter list
*/
public List<Filter> getFilters(String url) {
return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, "GET")
.getRequest())));
}
/**
* @return the list of {@code SecurityFilterChain}s which will be matched against and
* applied to incoming requests.
*/
public List<SecurityFilterChain> getFilterChains() {
return Collections.unmodifiableList(filterChains);
}
private static class VirtualFilterChain implements FilterChain {
//鏈條中的節(jié)點全部執(zhí)行完后榆综,處理request請求的對象
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 {
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);
nextFilter.doFilter(request, response, this);
}
}
}
}
比較關(guān)鍵的變量是 FILTER_APPLIED
和 filterChains
妙痹,前者在 doFilter
中防止2次處理铸史,后者存儲了需要代理的所有 Filter 并在 doFilterInternal
中選擇符合 url 條件的 Filter
運行。
內(nèi)部類 VirtualFilterChain
繼承自 FilterChain
怯伊,使用的是責任鏈模式琳轿。如下圖所示:
看源代碼如何實現(xiàn)責任鏈模式。
private static class VirtualFilterChain implements FilterChain {
//鏈條中的節(jié)點全部執(zhí)行完后,處理request請求的對象
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 {
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);
nextFilter.doFilter(request, response, this);
}
}
}
在調(diào)用 nextFilter.doFilter(request, response, this)
時崭篡,會把自身當作 FilterChain
傳入 nextFilter
中挪哄,這樣,只要 nextFilter
調(diào)用 filterChain.doFilter
琉闪,就會重新回到當前 VirtualFilterChain
迹炼,并選擇下一個 Filter
執(zhí)行。執(zhí)行圖如下所示:
spring security core filter 組件分析
在 Spring web Security 中颠毙,spring security core filter 以責任鏈的模式注冊在 FilterChainProxy
中斯入,按照順序依次是:
1. WebAsyncManagerIntegrationFilter
2. SecurityContextPersistenceFilter
3. HeaderWriterFilter
4. CsrfFilter
5. LogoutFilter
6. UsernamePasswordAuthenticationFilter
7. RequestCacheAwareFilter
8. SecurityContextHolderAwareRequestFilter
9. AnonymousAuthenticationFilter
10.SessionManagementFilter
11.ExceptionTranslationFilter
12.FilterSecurityInterceptor
和登陸息息相關(guān)的依次是 5,6,9,11,12
,下面依次分析這些組件蛀蜜。
LogoutFilter
LogoutFilter
實現(xiàn)的功能比較簡單刻两,主要是當遇到 logoutUrl
的時候進行退出的操作,并且跳轉(zhuǎn)到規(guī)定界面滴某。
觀察源碼磅摹,如下:
public class LogoutFilter extends GenericFilterBean {
// ~ Instance fields
// ================================================================================================
private RequestMatcher logoutRequestMatcher;
private final LogoutHandler handler;
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) {
this.handler = new CompositeLogoutHandler(handlers);
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
this.handler = new CompositeLogoutHandler(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");
}
this.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);
}
}
首先看 doFilter()
函數(shù),主要邏輯偽代碼是:
//step 1.1
if (requiresLogout(request)) {
//step 1.2
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
this.handler.logout(request, response, auth);
//step 1.3
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
} else {
//step 2.1
chain.doFilter(request, response);
}
主要分為 4 步:
- step 1.1:匹配 request 是否為 logout url
- step 1.2:利用 handler 實現(xiàn)退出邏輯
- step 1.3:執(zhí)行
logoutSuccessHanler
的onLogoutSuccess
函數(shù)霎奢,直接返回- step 2.1:為匹配到 logout url户誓,進入下一個責任鏈的
Filter
變量 logoutRequestMatcher
用于匹配 url。
變量 handler
用于處理登出邏輯椰憋。在標準 LogoutHandler
實現(xiàn)中厅克,使用了 CompositeLogoutHandler
,定義如下:
public final class CompositeLogoutHandler implements LogoutHandler {
private final List<LogoutHandler> logoutHandlers;
public CompositeLogoutHandler(LogoutHandler... logoutHandlers) {
Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
this.logoutHandlers = Arrays.asList(logoutHandlers);
}
public CompositeLogoutHandler(List<LogoutHandler> logoutHandlers) {
Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
this.logoutHandlers = logoutHandlers;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
for (LogoutHandler handler : this.logoutHandlers) {
handler.logout(request, response, authentication);
}
}
}
CompositeLogoutHandler
實現(xiàn)了 LogoutHandler
橙依,并且用責任鏈的形式執(zhí)行退出邏輯证舟。在標準實現(xiàn)中,CsrfLogoutHandler
和 SecurityContextLogoutHandler
被用在了責任鏈中窗骑。主要看 SecurityContextLogoutHandler
的形式女责。
public class SecurityContextLogoutHandler implements LogoutHandler {
private boolean invalidateHttpSession = true;
private boolean clearAuthentication = true;
// ~ Methods
// ========================================================================================================
/**
* Requires the request to be passed in.
*
* @param request from which to obtain a HTTP session (cannot be null)
* @param response not used (can be <code>null</code>)
* @param authentication not used (can be <code>null</code>)
*/
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
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();
}
public boolean isInvalidateHttpSession() {
return invalidateHttpSession;
}
/**
* Causes the HttpSession to be invalidated when this LogoutHandler is invoked. Defaults to true.
*/
public void setInvalidateHttpSession(boolean invalidateHttpSession) {
this.invalidateHttpSession = invalidateHttpSession;
}
/**
* If true, removes the Authentication from the SecurityContext to prevent issues with concurrent requests.
*/
public void setClearAuthentication(boolean clearAuthentication) {
this.clearAuthentication = clearAuthentication;
}
}
由此可見,主要是清空 session 和 SecurityContextHolder创译。
logoutSuccessHandler
變量實現(xiàn)了在登陸成功后實現(xiàn)的邏輯抵知。默認的變量使用 SimpleUrlLogoutSuccessHandler
,可以實現(xiàn)路徑的跳轉(zhuǎn)软族。定義如下:
public class SimpleUrlLogoutSuccessHandler extends
AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
super.handle(request, response, authentication);
}
}
AbstractAuthenticationTargetUrlRequestHandler
實現(xiàn)了跳轉(zhuǎn)的功能刷喜。
UsernamePasswordAuthenticationFilter
復(fù)雜的一個類,主要實現(xiàn)了登陸邏輯立砸。UsernamePasswordAuthenticationFilter
繼承自 AbstractAuthenticationProcessingFilter
掖疮,AbstractAuthenticationProcessingFilter
實現(xiàn)了 doFilter()
方法,UsernamePasswordAuthenticationFilter
是使用的 AbstractAuthenticationProcessingFilter
的 doFilter()
方法颗祝。首先看 doFilter()
方法浊闪。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//step 1
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
//step 2
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
//step 3
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
//step 4
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
//step 5
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//step 6
successfulAuthentication(request, response, chain, authResult);
}
AbstractAuthenticationProcessingFilter
主要實現(xiàn)了 6 個步驟恼布,如下:
- step 1:匹配是否為登陸界面并且為POST
- step 2:嘗試進行驗證并執(zhí)行與會話相關(guān)的操作
- step 3:當拋出
InternalAuthenticationServiceException
錯誤時,執(zhí)行unsuccessfulAuthentication
函數(shù)并返回- step 4:當拋出
AuthenticationException
錯誤搁宾,即驗證失敗時折汞,執(zhí)行unsuccessfulAuthentication
函數(shù)并返回- step 5:當
ontinueChainBeforeSuccessfulAuthentication
為true
時,執(zhí)行下一個Filter
的函數(shù)- step 6:執(zhí)行
successfulAuthentication()
函數(shù)盖腿。
step 2 執(zhí)行 attemptAuthentication()
函數(shù)爽待,是一個抽象函數(shù),具體的實現(xiàn)在 UsernamePasswordAuthenicationFilter
中翩腐。
note
attemptAuthentication()
必須返回一個已驗證用戶填充的Authentication
堕伪,表明驗證通過。- 或者返回一個
null
表明表示身份驗證過程仍在進行中栗菜。在返回之前欠雌,實現(xiàn)應(yīng)該執(zhí)行完成流程所需的任何額外工作。- 如果身份驗證過程失敗疙筹,拋出
AuthenticationException
異常富俄。
UsernamePasswordAuthenicationFilter
的 attemptAuthentication
實現(xiàn)如下:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
obtainXXX()
函數(shù)通過使用 request.getParameter("XXX")
獲得參數(shù),最重要的邏輯在 this.getAuthenticationManager().authenticate(authRequest)
中而咆。
在介紹具體的驗證邏輯之前霍比,介紹幾個基本的概念和類。
首先是 Autnenication
接口暴备,這個接口就是我們所說的令牌悠瞬,保存了該用戶的 principal、credential涯捻、details浅妆、authorities。就用戶名密碼登陸來說障癌,principal 就是用戶名凌外,credential 就是密碼,details 就是 IP 之類的詳細信息涛浙,authorities 代表的就是被授權(quán)的權(quán)利康辑。部分如下:
public interface Authentication extends Principal, Serializable {
// ~ Methods
// ========================================================================================================
/**
* Set by an AuthenticationManager to indicate the authorities that the
* principal has been granted. Implementations should ensure that modifications to the returned collection array do not affect the state of the Authentication object, or use an unmodifiable instance.
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* The credentials that prove the principal is correct. This is usually a password, but could be anything relevant to the AuthenticationManager. Callers are expected to populate the credentials.
*/
Object getCredentials();
Object getDetails();
/**
* The identity of the principal being authenticated. In the case of an authentication request with username and password, this would be the username. Callers are expected to populate the principal for an authentication request.
* Many of the authentication providers will create a code UserDetails object as the principal.
*/
Object getPrincipal();
/**
* Used to indicate to AbstractSecurityInterceptor whether it should present
* the authentication token to the AuthenticationManager. Typically an
* AuthenticationManage(or, more often, one of its
* AuthenticationProviders) will return an immutable authentication token
* after successful authentication, in which case that token can safely return
* true to this method. Returning <code>true</code> will improve
* performance, as calling the <code>AuthenticationManager</code> for every request
* will no longer be necessary.
*/
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
在 Authentication
的抽象實現(xiàn) AbstractAuthenticationToken
中,賦予了 principal
更多的意義轿亮,一般使用 userDetails
類作為 principal
疮薇,已存儲更多的信息,如被授予的權(quán)限我注、用戶名按咒、密碼、是否賬號過期仓手、是否賬號被鎖胖齐、賬號是否可用等信息。
UsernamePasswordAuthenticationToken
實現(xiàn)了 AbstractAuthenticationToken
嗽冒。
AuthenticationManager
呀伙,是主要的驗證方法。
定義如下:
public interface AuthenticationManager {
// ~ Methods
// ========================================================================================================
/**
* Attempts to authenticate the passed Authentication object, returning a
* fully populated Authentication object (including granted authorities)
* if successful.
*
* @param authentication the authentication request object
*
* @return a fully authenticated object including credentials
*
* @throws AuthenticationException if authentication fails
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager
的實現(xiàn)必須遵循以下的原則:
- 當賬戶不能使用并且
AuthenticationManager
可以檢測到這個狀態(tài)時必須拋出DisabledException
(A DisabledException must be thrown if an account is disabled and the AuthenticationManager can test for this state)- 當賬戶被鎖定并且
AuthenticationManager
可以檢測到這個狀態(tài)時必須拋出LockedException
錯誤添坊。(A LockedException must be thrown if an account is locked and the AuthenticationManager can test for account locking)- 如果出現(xiàn)不正確的憑證剿另,則必須拋出
BadCredentialsException
(A BadCredentialsException must be thrown if incorrect credentials arepresented. Whilst the above exceptions are optional, an AuthenticationManager must always test credentials)- 異常應(yīng)該按照上述順序進行測試(例如,如果帳戶被禁用或鎖定贬蛙,則立即拒絕身份驗證請求雨女,且不執(zhí)行憑據(jù)測試過程),并在適用的情況下拋出異常阳准。此證書將針對禁用或鎖定帳戶進行測試(Exceptions should be tested for and if applicable thrown in the order expressedabove (i.e. if an account is disabled or locked, the authentication request isimmediately rejected and the credentials testing process is not performed). Thisprevents credentials being tested against disabled or locked accounts)
在 spring security 中氛堕,AuthenticationManager
只有一個實現(xiàn),即 ProviderManager
野蝇。在 ProviderManager
中讼稚,最重要的是維持了一個 AuthenticationProvider
列表。
AuthenticationProvider
接口定義如下:
public interface AuthenticationProvider {
//和AuthenticationManager相同
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//如果此AuthenticationProvider支持指定的身份驗證對象绕沈,則返回true锐想。
//返回true并不保證AuthenticationProvider能夠?qū)ι矸蒡炞C類的呈現(xiàn)實例進行身份驗證。它只是表明它可以支持對其進行更密切的評估乍狐。AuthenticationProvider仍然可以從authenticate(Authentication)方法返回null赠摇,以指示應(yīng)該嘗試另一個AuthenticationProvider。
//選擇能夠執(zhí)行身份驗證的AuthenticationProvider是在ProviderManager運行時進行的浅蚪。
boolean supports(Class<?> authentication);
}
看 ProviderManager
的 authenticate
方法藕帜,如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
具體的實現(xiàn)邏輯是調(diào)用支持該 Authentication
的 AuthenticationProvider
進行驗證,如果遇到賬號被鎖定或者被禁用(即拋出 AccountStatusException
異常惜傲,這是 DisabledException
和 LockedException
的父類)耘戚,如果驗證不正確,即遇到 AuthenticationException
異常操漠,則記錄最新異常到 lastException
收津,并執(zhí)行下一個 provider
。所有 provider
執(zhí)行完成之后浊伙,如果 result
為空撞秋,則說明沒有驗證通過,如果存在 AuthenticationManager parent
嚣鄙,則嘗試執(zhí)行 parent
的驗證函數(shù)吻贿。如果這一步執(zhí)行完之后 result
不為空,則返回 result
哑子,否則拋出 lastException
舅列。
Spring 默認使用 DaoThenticationProvider
來實現(xiàn) AuthenticationProvider
肌割。DaoAuthenticationProvider
繼承自 AbstractUserDetailsAuthenticationProvider
。AbstractUserDetailsAuthenticationProvider
的 Authenticate()
方法如下(只支持 UsernamePasswordAuthenticationToken
):
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
處理邏輯是首先根據(jù) Token authentication
獲得用戶名帐要,然后判斷是否有 UserDetail
緩存把敞,如果沒有,通過 retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)
檢索得到 userDetail
榨惠,如果沒有找到奋早,拋出 UsernameNotFoundException
異常或者 BadCredentialsException
異常赠橙。當有 userDetail
之后耽装,調(diào)用 DefaultPreAuthenticationChecks
的 check()
函數(shù),即 preAuthenticationChecks.check(user)
用來測試 userDetail
賬號是否被鎖期揪,賬號是否不可用掉奄,賬號是否過期,并拋出相應(yīng)錯誤凤薛。完成之后挥萌,執(zhí)行 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication)
,這個函數(shù)是一個抽象函數(shù)枉侧,要求子類實現(xiàn)引瀑,增加更多的檢測。如果拋出 AuthenticationException
異常榨馁,并且是從緩存中獲得 userDetail
的話憨栽,會重新調(diào)用 retrieveUser
重新檢測,如果都不通過翼虫,才徹底拋出異常屑柔。如果沒有拋出,會執(zhí)行 postAuthenticationChecks.checks(user)
珍剑,默認實現(xiàn)是 DefaultPostAuthenticationChecks
主要是檢查 密鑰是否過期掸宛,如過期,拋出 CredentialsExpiredException
異常招拙。最后調(diào)用 createSuccessAuthentication
創(chuàng)建 UsernamePasswordAuthenticationToken
并返回唧瘾。
createSuccessAuthentication()
函數(shù)如下:
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
注意 CredentialsExpiredException
棕硫、LockedException
岸裙、DisabledException
、AccountExpiredException
都是 AccountStatusException 的子類贰拿,都會被 ProviderManager
捕獲并且直接拋出錯誤规哪。
DaoAuthenticationProvider
實現(xiàn)了 additionalAuthenticationChecks()
求豫、 retrieveUser()
函數(shù),并且改寫了 createSuccessAuthentication()
函數(shù)。
additionalAuthenticationChecks()
如下:
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
如上蝠嘉,additionalAuthenticationChecks()
主要是增加了密碼驗證的邏輯最疆,如果驗證不通過,拋出 BadCredentialsException
錯誤蚤告。
retrieveUser()
函數(shù)主要實現(xiàn) userDetail
的獲取努酸。
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
如上所示,主要是通過 this.getUserDetailsService().loadUserByUsername(username)
函數(shù)獲取罩缴,this.getUserDetailsService()
返回一個 UserDetailsService
接口實現(xiàn) UserDetail
的查詢。
createSuccessAuthentication()
主要增加了可否使用增強密鑰的判斷层扶,增加了安全性箫章。
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
UserDetailsService
接口最常見的實現(xiàn)為 InMemoryUserDetailsManager
和 JdbcUserDetailsManager
,前者在內(nèi)存中維護一個 <<String -> UserDetail>>
映射镜会,后者直接從數(shù)據(jù)庫中讀取數(shù)據(jù)檬寂,前者的 loadUserByUsername
實現(xiàn)如下:
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
用于驗證的接口及類圖如下所示:
驗證流程如下:
回到 UsernamePasswordAuthenticationFilter
,我們知道戳表,當驗證失敗時桶至,會拋出三種錯誤,第一種為 AccountStatusException
匾旭,第二種為 InternalAuthenticationServiceException
镣屹,第三種為 AuthenticationException
。在 UsernamePasswordAuthenticationFilter
的第三步价涝,第四步女蜈,分別用兩個 catch
語句塊進行捕捉,進行錯誤處理然后直接返回色瘩,如下:
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
//step 4
// AccountStatusException 為 AuthenticationException 子類伪窖,這個捕捉函數(shù)可以捕捉到 AccountStatusException 和 AuthenticationException 兩種異常
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
unsucessfulAuthentication()
函數(shù)的功能較簡單,即調(diào)用 rememberMeServices.loginFail
和 failureHandler.onAuthenticationFailure
設(shè)置失敗的操作居兆。
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
重點看 failureHandler.onAuthenticationFailure
覆山。
failureHander
是實現(xiàn)了 AuthenticationFailureHandler
的類,默認實現(xiàn)是 SimpleUrlAuthenticationFailureHandler
泥栖,onAuthenticationFailure
函數(shù)如下簇宽,可以看出其主要目的是實現(xiàn)跳轉(zhuǎn)或者重定向。
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
} else {
saveException(request, exception);
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
} else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
UsernamePasswordAuthenticationFilter
的第六步即認證成功后的護理吧享,主要是調(diào)用 successfulAuthentication()
函數(shù)進行處理晦毙。函數(shù)如下:
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
主要工作就是把密鑰存儲在 SecurityContextHolder
上,并調(diào)用 successHandler.onAuthenticationSuccess()
實現(xiàn)相關(guān)的操作耙蔑。與錯誤情況類似见妒,登陸成功也主要是進行一些跳轉(zhuǎn)。
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter
的邏輯很簡單,當在 SecurityContextHolder
中沒有值時须揣,就創(chuàng)建一個匿名的 Token盐股,傳遞到下一個 Filter
,代碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
} else {}
chain.doFilter(req, res);
}
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
ExceptionTranslationFilter
FilterSecurityInterceptor
FilterSecurityInterceptor
繼承自 AbstractSecurityInterceptor
耻卡。主要作用是通過 Filter
接口實現(xiàn)對 http 資源的控制疯汁。
FilterSecurityInterceptor
的 doFilter
函數(shù)如下:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
其中,FilterInvocation
的主要作用是對 request, response, chain
的封裝卵酪。重點函數(shù)還是在 invoke()
上幌蚊。 invoke()
函數(shù)如下所示:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
// step 1
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 {
// step 2
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// step 3
InterceptorStatusToken token = super.beforeInvocation(fi);
// step 4
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
// step 5
super.afterInvocation(token, null);
}
}
其中,step 1,2 的作用主要是判斷是否是一次應(yīng)用并且已經(jīng)應(yīng)用的 request溃卡,如果是則直接進入下一個 Filter
溢豆,如果不是并且還沒有應(yīng)用,則設(shè)應(yīng)用標志為 true
進行處理瘸羡。step 3 對 fi 做驗證漩仙,step 4,5 對 fi做一些其他的處理。
beforeInvocation()
的實現(xiàn)在父類 AbstractSecurityIntercepter
中犹赖,如下:
protected InterceptorStatusToken beforeInvocation(Object object) {
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);
}
}
主要是調(diào)用 this.accessDecisionManager.decide(authenticated, object, attributes)
進行授權(quán)队他。