Spring 源码分析(五)Sercurity
sschrodienger
2019/03/04
Filter 代理组件分析
DelegatingFilterProxy
DelegatingFilterProxy
是标准 servlet
过滤器的一个代理类,它可以代理 Spring 容器中实现了 Filter
接口的 Bean,以方便该过滤器获得 Spring 依赖注入以及生命周期的支持。
DelegatingFilterProxy
维护了一个代理类 private volatile Filter delegate
,delegate
是最重要的执行过滤器。
@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()
方法主要是调用 invokeDelegate()
方法 来执行代理的 doFilter()
方法。
FilterChainProxy
在 DelegatingFilterProxy
中,delegate
变量的实际类型是 FilterChainProxy
。关键代码如下:
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);
//如果得到的过滤器的数量为零,则直接跳过
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 {
//链条中的节点全部执行完后,处理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);
}
}
}
}
比较关键的变量是 FILTER_APPLIED
和 filterChains
,前者在 doFilter
中防止2次处理,后者存储了需要代理的所有 Filter 并在 doFilterInternal
中选择符合 url 条件的 Filter
运行。
内部类 VirtualFilterChain
继承自 FilterChain
,使用的是责任链模式。如下图所示:
看源代码如何实现责任链模式。
private static class VirtualFilterChain implements FilterChain {
//链条中的节点全部执行完后,处理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);
}
}
}
在调用 nextFilter.doFilter(request, response, this)
时,会把自身当作 FilterChain
传入 nextFilter
中,这样,只要 nextFilter
调用 filterChain.doFilter
,就会重新回到当前 VirtualFilterChain
,并选择下一个 Filter
执行。执行图如下所示:
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
和登陆息息相关的依次是 5,6,9,11,12
,下面依次分析这些组件。
LogoutFilter
LogoutFilter
实现的功能比较简单,主要是当遇到 logoutUrl
的时候进行退出的操作,并且跳转到规定界面。
观察源码,如下:
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()
函数,主要逻辑伪代码是:
//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 实现退出逻辑
- step 1.3:执行
logoutSuccessHanler
的onLogoutSuccess
函数,直接返回- step 2.1:为匹配到 logout url,进入下一个责任链的
Filter
变量 logoutRequestMatcher
用于匹配 url。
变量 handler
用于处理登出逻辑。在标准 LogoutHandler
实现中,使用了 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
实现了 LogoutHandler
,并且用责任链的形式执行退出逻辑。在标准实现中,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
变量实现了在登陆成功后实现的逻辑。默认的变量使用 SimpleUrlLogoutSuccessHandler
,可以实现路径的跳转。定义如下:
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
实现了跳转的功能。
UsernamePasswordAuthenticationFilter
复杂的一个类,主要实现了登陆逻辑。UsernamePasswordAuthenticationFilter
继承自 AbstractAuthenticationProcessingFilter
,AbstractAuthenticationProcessingFilter
实现了 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
主要实现了 6 个步骤,如下:
- step 1:匹配是否为登陆界面并且为POST
- step 2:尝试进行验证并执行与会话相关的操作
- step 3:当抛出
InternalAuthenticationServiceException
错误时,执行unsuccessfulAuthentication
函数并返回- step 4:当抛出
AuthenticationException
错误,即验证失败时,执行unsuccessfulAuthentication
函数并返回- step 5:当
ontinueChainBeforeSuccessfulAuthentication
为true
时,执行下一个Filter
的函数- step 6:执行
successfulAuthentication()
函数。
step 2 执行 attemptAuthentication()
函数,是一个抽象函数,具体的实现在 UsernamePasswordAuthenicationFilter
中。
note
attemptAuthentication()
必须返回一个已验证用户填充的Authentication
,表明验证通过。- 或者返回一个
null
表明表示身份验证过程仍在进行中。在返回之前,实现应该执行完成流程所需的任何额外工作。- 如果身份验证过程失败,抛出
AuthenticationException
异常。
UsernamePasswordAuthenicationFilter
的 attemptAuthentication
实现如下:
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()
函数通过使用 request.getParameter("XXX")
获得参数,最重要的逻辑在 this.getAuthenticationManager().authenticate(authRequest)
中。
在介绍具体的验证逻辑之前,介绍几个基本的概念和类。
首先是 Autnenication
接口,这个接口就是我们所说的令牌,保存了该用户的 principal、credential、details、authorities。就用户名密码登陆来说,principal 就是用户名,credential 就是密码,details 就是 IP 之类的详细信息,authorities 代表的就是被授权的权利。部分如下:
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
的抽象实现 AbstractAuthenticationToken
中,赋予了 principal
更多的意义,一般使用 userDetails
类作为 principal
,已存储更多的信息,如被授予的权限、用户名、密码、是否账号过期、是否账号被锁、账号是否可用等信息。
UsernamePasswordAuthenticationToken
实现了 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
的实现必须遵循以下的原则:
- 当账户不能使用并且
AuthenticationManager
可以检测到这个状态时必须抛出DisabledException
(A DisabledException must be thrown if an account is disabled and the AuthenticationManager can test for this state)- 当账户被锁定并且
AuthenticationManager
可以检测到这个状态时必须抛出LockedException
错误。(A LockedException must be thrown if an account is locked and the AuthenticationManager can test for account locking)- 如果出现不正确的凭证,则必须抛出
BadCredentialsException
(A BadCredentialsException must be thrown if incorrect credentials arepresented. Whilst the above exceptions are optional, an AuthenticationManager must always test credentials)- 异常应该按照上述顺序进行测试(例如,如果帐户被禁用或锁定,则立即拒绝身份验证请求,且不执行凭据测试过程),并在适用的情况下抛出异常。此证书将针对禁用或锁定帐户进行测试(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
只有一个实现,即 ProviderManager
。在 ProviderManager
中,最重要的是维持了一个 AuthenticationProvider
列表。
AuthenticationProvider
接口定义如下:
public interface AuthenticationProvider {
//和AuthenticationManager相同
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//如果此AuthenticationProvider支持指定的身份验证对象,则返回true。
//返回true并不保证AuthenticationProvider能够对身份验证类的呈现实例进行身份验证。它只是表明它可以支持对其进行更密切的评估。AuthenticationProvider仍然可以从authenticate(Authentication)方法返回null,以指示应该尝试另一个AuthenticationProvider。
//选择能够执行身份验证的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;
}
具体的实现逻辑是调用支持该 Authentication
的 AuthenticationProvider
进行验证,如果遇到账号被锁定或者被禁用(即抛出 AccountStatusException
异常,这是 DisabledException
和 LockedException
的父类),如果验证不正确,即遇到 AuthenticationException
异常,则记录最新异常到 lastException
,并执行下一个 provider
。所有 provider
执行完成之后,如果 result
为空,则说明没有验证通过,如果存在 AuthenticationManager parent
,则尝试执行 parent
的验证函数。如果这一步执行完之后 result
不为空,则返回 result
,否则抛出 lastException
。
Spring 默认使用 DaoThenticationProvider
来实现 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);
}
处理逻辑是首先根据 Token authentication
获得用户名,然后判断是否有 UserDetail
缓存,如果没有,通过 retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)
检索得到 userDetail
,如果没有找到,抛出 UsernameNotFoundException
异常或者 BadCredentialsException
异常。当有 userDetail
之后,调用 DefaultPreAuthenticationChecks
的 check()
函数,即 preAuthenticationChecks.check(user)
用来测试 userDetail
账号是否被锁,账号是否不可用,账号是否过期,并抛出相应错误。完成之后,执行 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication)
,这个函数是一个抽象函数,要求子类实现,增加更多的检测。如果抛出 AuthenticationException
异常,并且是从缓存中获得 userDetail
的话,会重新调用 retrieveUser
重新检测,如果都不通过,才彻底抛出异常。如果没有抛出,会执行 postAuthenticationChecks.checks(user)
,默认实现是 DefaultPostAuthenticationChecks
主要是检查 密钥是否过期,如过期,抛出 CredentialsExpiredException
异常。最后调用 createSuccessAuthentication
创建 UsernamePasswordAuthenticationToken
并返回。
createSuccessAuthentication()
函数如下:
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
实现了 additionalAuthenticationChecks()
、 retrieveUser()
函数,并且改写了 createSuccessAuthentication()
函数。
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()
函数主要实现 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)
函数获取,this.getUserDetailsService()
返回一个 UserDetailsService
接口实现 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
接口最常见的实现为 InMemoryUserDetailsManager
和 JdbcUserDetailsManager
,前者在内存中维护一个 <<String -> UserDetail>>
映射,后者直接从数据库中读取数据,前者的 loadUserByUsername
实现如下:
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 子类,这个捕捉函数可以捕捉到 AccountStatusException 和 AuthenticationException 两种异常
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
unsucessfulAuthentication()
函数的功能较简单,即调用 rememberMeServices.loginFail
和 failureHandler.onAuthenticationFailure
设置失败的操作。
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
是实现了 AuthenticationFailureHandler
的类,默认实现是 SimpleUrlAuthenticationFailureHandler
,onAuthenticationFailure
函数如下,可以看出其主要目的是实现跳转或者重定向。
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
的第六步即认证成功后的护理,主要是调用 successfulAuthentication()
函数进行处理。函数如下:
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
上,并调用 successHandler.onAuthenticationSuccess()
实现相关的操作。与错误情况类似,登陆成功也主要是进行一些跳转。
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter
的逻辑很简单,当在 SecurityContextHolder
中没有值时,就创建一个匿名的 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
接口实现对 http 资源的控制。
FilterSecurityInterceptor
的 doFilter
函数如下:
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
的封装。重点函数还是在 invoke()
上。 invoke()
函数如下所示:
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 的作用主要是判断是否是一次应用并且已经应用的 request,如果是则直接进入下一个 Filter
,如果不是并且还没有应用,则设应用标志为 true
进行处理。step 3 对 fi 做验证,step 4,5 对 fi做一些其他的处理。
beforeInvocation()
的实现在父类 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);
}
}
主要是调用 this.accessDecisionManager.decide(authenticated, object, attributes)
进行授权。