Spring 源码分析(四)Sercurity
sschrodinger
2019/03/04
登陆验证流程
假设有如下的 spring security 配置。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("user_1").password("user_1").roles("USER")
.and()
.withUser("user_2").password("user_2").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.permitAll();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
输入任意 url (非/login),流程如下:
step 1
在 Spring 中,登陆验证的逻辑始于 DelegatingFilterProxy
,这是一个继承了 Filter
的类,用于实现 Java Servlet 规范的过滤器。
DelegatingFilterProxy
的 doFilter()
方法如下:
@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);
}
最重要的是 invokeDelegate()
方法,Spring 所有的 Filter 逻辑都是在 invokeDelegate()
方法中完成的,所以没有 filterChain.doFilter(request, response, filterChain)
将处理过程抛出到下一 Filter。
参数 delefateToUse
包括了所有的过滤器,调试信息如下:
delegate FilterChainProxy (id=124)
|-...//其他
|-beanName "springSecurityFilterChain" (id=126)
|-filterChains ArrayList<E> (id=133)
|-elementData Object[1] (id=158)
|-[0] DefaultSecurityFilterChain (id=3344)
|-org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@630e5010,
|-org.springframework.security.web.context.SecurityContextPersistenceFilter@4eab9aec,
|-org.springframework.security.web.header.HeaderWriterFilter@533e8807,
|-org.springframework.security.web.csrf.CsrfFilter@156eeff1,
|-org.springframework.security.web.authentication.logout.LogoutFilter@656c0eae,
|-org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@267b678f,
|-org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2c6efee3,
|-org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@56adbb07,
|-org.springframework.security.web.authentication.AnonymousAuthenticationFilter@10b5ff4d,
|-org.springframework.security.web.session.SessionManagementFilter@2b4ba2d9,
|-org.springframework.security.web.access.ExceptionTranslationFilter@2bc0603f,
|-org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4e826fd4
Spring security 的验证工作都是在 DelegatingFilterProxy
中的一个名为 springSecurityFilterChain
中完成的。
我们看 invokeDelegate()
的源码,如下:
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
step 2
第二步进入 delegate
的 doFilter()
方法执行。
delegate
的本质是继承了 Filter
的类。维护了一个 SecurityFilterChain
列表。SecurityFilterChain
定义如下:
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}
在当前环境中,List<SecurityFilterChain>
只维护了一个元素。即
@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);
}
我们看 doFilter()
方法,首先,判断 request
是否有 FILTER_APPLIED
属性,如果没有,则需要添加,然后执行 doFilterInternal()
方法。
getFilters(fwRequest)
通过 url
得到符合条件的所有 filterChain
的第一个 Filter
。即会得到如下 12 个 Filter
:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter,
org.springframework.security.web.context.SecurityContextPersistenceFilter,
org.springframework.security.web.header.HeaderWriterFilter,
org.springframework.security.web.csrf.CsrfFilter,
org.springframework.security.web.authentication.logout.LogoutFilter,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter,
org.springframework.security.web.session.SessionManagementFilter,
org.springframework.security.web.access.ExceptionTranslationFilter,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
变量chain
代表原始 FilterChain
,当 getFilters(fwRequest)
返回不为 null
时,返回值和 chain
会组合成 VirtualFilterChain
,构造函数如下:
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
VirtualFilterChain
的 doFilter
函数如下:
@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 {
//currrent addition filter pos, init is zero
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
注意,当执行 VirtualFilterChain
的 doFilter
函数时,会先执行 else 语句,currentPosition
先自加,然后将自身作为参数执行 nextFilter
的 dofilter()
方法,这样就会重复执行 else 语句,将所有的 additionalFilter
全部执行完。
step 3
执行所有 VirtualFilterChain
中的 Filter
。
nextFilter()
依次执行如下 12 个 Filter
。
1.WebAsyncManagerIntegrationFilter
2.SecurityContextPersistenceFilter
3.HeaderWriterFilter
4.CsrfFilter
5.LogoutFilter
6.UsernamePasswordAuthenticationFilter
7.RequestCacheAwareFilter
8.SecurityContextHolderAwareRequestFilter
9.AnonymousAuthenticationFilter
10.SessionManagementFilter
11.ExceptionTranslationFilter
12.FilterSecurityInterceptor
其中比较重要的是2,5,6,9,12三个 filter
。
SecurityContextPersistenceFilter
主要是对保存的密码信息等做持久化处理。
LogoutFilter
主要是匹配退出 url 并作退出处理逻辑。
UsernamePasswordAuthenticationFilter
主要作用是当匹配登录界面时,做一些登陆的操作,否则直接到下一过滤器。
AnonymousAuthenticationFilter
主要作用是当没有进行登陆时,自动创建匿名用户。
FilterSecurityInterceptor
继承了 AbstractSecurityInterceptor
,主要用于登陆的核心逻辑。
note(AbstractSecurityInterceptor api note)
- Abstract class that implements security interception for secure objects.
- The AbstractSecurityInterceptor will ensure the proper startupconfiguration of the security interceptor. It will also implement the proper handlingof secure object invocations, namely:
- Obtain the Authentication object from the SecurityContextHolder.
- Determine if the request relates to a secured or public invocation by looking upthe secure object request against the SecurityMetadataSource.
- For an invocation that is secured (there is a list of ConfigAttributesfor the secure object invocation):
- If either the org.springframework.security.core.Authentication.isAuthenticated() returns false, or the alwaysReauthenticate is true,authenticate the request against the configured AuthenticationManager. Whenauthenticated, replace the Authentication object on the SecurityContextHolder with the returned value.
- Authorize the request against the configured AccessDecisionManager.
- Perform any run-as replacement via the configured RunAsManager.
- Pass control back to the concrete subclass, which will actually proceed withexecuting the object. A InterceptorStatusToken is returned so that after thesubclass has finished proceeding with execution of the object, its finally clause canensure the AbstractSecurityInterceptor is re-called and tidies upcorrectly using finallyInvocation(InterceptorStatusToken).
- The concrete subclass will re-call the AbstractSecurityInterceptor viathe afterInvocation(InterceptorStatusToken, Object) method.
- If the RunAsManager replaced the Authentication object,return the SecurityContextHolder to the object that existed after the callto AuthenticationManager.
- If an AfterInvocationManager is defined, invoke the invocation managerand allow it to replace the object due to be returned to the caller.
- For an invocation that is public (there are no ConfigAttributes forthe secure object invocation):
- As described above, the concrete subclass will be returned an InterceptorStatusToken which is subsequently re-presented to the AbstractSecurityInterceptor after the secure object has been executed. The AbstractSecurityInterceptor will take no further action when its afterInvocation(InterceptorStatusToken, Object) is called.
- Control again returns to the concrete subclass, along with the Objectthat should be returned to the caller. The subclass will then return that result orexception to the original caller.
FilterSecurityInterceptor
的 invoke()
方法如下:
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 && observeOncePerRequest) {
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);
}
}
beforeInvocation()
函数的处理逻辑是先验证,如果不满足条件,则抛出 accessDeniedException
错误,否则生成 InterceptorStatusToken
给上一级。
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
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 (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);
}
}
ExceptionTranslationFilter
会捕捉 FilterSecurityInterceptor
抛出的错误,并进行错误处理。错误处理语句如下:
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) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
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);
}
}
错误的主要处理过程在 handleSpringSecurityException(request, response, chain, ase)
中,如下:
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
} else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); "redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
} else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
在 sendStartAuthentication
函数中,会对连接进行重定向到规定的登陆界面。
如在 /login 界面利用 POST
提交表单信息登陆。流程的 step 1 和 step 2 和上相同,step 3 略有区别。在执行到 UsernamePasswordAuthenticationFilter
时,就会进行登陆逻辑,代码如下:
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;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
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);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
具体逻辑是 attemptAuthentication()
函数进行验证,当验证成功时,执行 successfulAuthentication(request, response, chain, authResult)
进行跳转,失败时,执行 unsuccessfulAuthentication(request, response, failed)
进行跳转,假如设置了跳转地址,则回继续调用 doFilter()
方法,在 FilterSecurityInterceptor
中利用UserNamePasswordToken 进行授权。
综上,时序图(关键类)如下: