Spring Cloud api-gateway using @EnableOAuth2Sso 注解。
Start API-GATEWAY application。
When request arrived,会进入ApplicationFilterChain的过滤器链。
ApplicationFilterChain位于tomcat-embed-core.jar的一下路径:
org.apache.catalina.core.ApplicationFilterChain.class
默认会进入ApplicationFilterChain的internalDoFilter method。
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = null;
try {
filter = filterConfig.getFilter();
support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
filter, request, response);
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege
("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response);
} catch (IOException | ServletException | RuntimeException e) {
if (filter != null)
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response, e);
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
if (filter != null)
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response, e);
throw new ServletException
(sm.getString("filterChain.filter"), e);
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
support.fireInstanceEvent(InstanceEvent.BEFORE_SERVICE_EVENT,
servlet, request, response);
if (request.isAsyncSupported()
&& !support.getWrapper().isAsyncSupported()) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse)) {
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
servlet.service(request, response);
}
} else {
servlet.service(request, response);
}
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response);
} catch (IOException e) {
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw e;
} catch (ServletException e) {
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw e;
} catch (RuntimeException e) {
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw new ServletException
(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}
此处会循环调用filters中的每个doFilter方法,如下所示:
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = null;
try {
filter = filterConfig.getFilter();
support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
filter, request, response);
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege
("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response);
} catch (IOException | ServletException | RuntimeException e) {
if (filter != null)
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response, e);
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
if (filter != null)
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response, e);
throw new ServletException
(sm.getString("filterChain.filter"), e);
}
return;
}
Filters如下所示:
0:ApplicationFilterConfig[name=metricFilter, filterClass=org.springframework.boot.actuate.autoconfigure.MetricsFilter]
1:ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.context.web.OrderedCharacterEncodingFilter]
2:ApplicationFilterConfig[name=hiddenHttpMethodFilter, filterClass=org.springframework.boot.context.web.OrderedHiddenHttpMethodFilter]
3:ApplicationFilterConfig[name=httpPutFormContentFilter, filterClass=org.springframework.boot.context.web.OrderedHttpPutFormContentFilter]
4:ApplicationFilterConfig[name=OAuth2ClientContextFilter, filterClass=org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter]
5: ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.context.web.OrderedRequestContextFilter]
6: ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.web.filter.DelegatingFilterProxy]
7:ApplicationFilterConfig[name=webRequestLoggingFilter, filterClass=org.springframework.boot.actuate.trace.WebRequestTraceFilter]
8:ApplicationFilterConfig[name=applicationContextIdFilter, filterClass=org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter]
9:ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]
此处关键的是OAuth2ClientContextFilter和springSecurityFilterChain。
OAuth2ClientContextFilter
Security filter for an OAuth2 client.
/**
* Security filter for an OAuth2 client.
*
* @author Ryan Heaton
* @author Dave Syer
*/
public class OAuth2ClientContextFilter implements Filter, InitializingBean {
/**
* Key in request attributes for the current URI in case it is needed by
* rest client code that needs to send a redirect URI to an authorization
* server.
*/
public static final String CURRENT_URI = "currentUri";
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public void afterPropertiesSet() throws Exception {
Assert.notNull(redirectStrategy,
"A redirect strategy must be supplied.");
}
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
request.setAttribute(CURRENT_URI, calculateCurrentUri(request));
try {
chain.doFilter(servletRequest, servletResponse);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
.getFirstThrowableOfType(
UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
redirectUser(redirect, request, response);
} else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new NestedServletException("Unhandled exception", ex);
}
}
}
/**
* Redirect the user according to the specified exception.
*
* @param e
* The user redirect exception.
* @param request
* The request.
* @param response
* The response.
*/
protected void redirectUser(UserRedirectRequiredException e,
HttpServletRequest request, HttpServletResponse response)
throws IOException {
String redirectUri = e.getRedirectUri();
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(redirectUri);
Map<String, String> requestParams = e.getRequestParams();
for (Map.Entry<String, String> param : requestParams.entrySet()) {
builder.queryParam(param.getKey(), param.getValue());
}
if (e.getStateKey() != null) {
builder.queryParam("state", e.getStateKey());
}
this.redirectStrategy.sendRedirect(request, response, builder.build()
.encode().toUriString());
}
/**
* Calculate the current URI given the request.
*
* @param request
* The request.
* @return The current uri.
*/
protected String calculateCurrentUri(HttpServletRequest request)
throws UnsupportedEncodingException {
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder
.fromRequest(request);
// Now work around SPR-10172...
String queryString = request.getQueryString();
boolean legalSpaces = queryString != null && queryString.contains("+");
if (legalSpaces) {
builder.replaceQuery(queryString.replace("+", "%20"));
}
UriComponents uri = null;
try {
uri = builder.replaceQueryParam("code").build(true);
} catch (IllegalArgumentException ex) {
// ignore failures to parse the url (including query string). does't
// make sense for redirection purposes anyway.
return null;
}
String query = uri.getQuery();
if (legalSpaces) {
query = query.replace("%20", "+");
}
return ServletUriComponentsBuilder.fromUri(uri.toUri())
.replaceQuery(query).build().toString();
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
this.throwableAnalyzer = throwableAnalyzer;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
}
该Filter在Filters中的position is 5.继续执行chain.doFilter(servletRequest, servletResponse);
此处的chain是ApplicationFilterChain,每次执行完一个Filter,都会回退到ApplicationFilterChain中获取下一个Filter,执行nextFilter.doFilter()。形成了一个过滤器的链式调用。
springSecurityFilterChain
class:org.springframework.web.filter.DelegatingFilterProxy,位于spring-web.jar包。
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* Proxy for a standard Servlet Filter, delegating to a Spring-managed bean that
* implements the Filter interface. Supports a "targetBeanName" filter init-param
* in {@code web.xml}, specifying the name of the target bean in the Spring
* application context.
*
* <p>{@code web.xml} will usually contain a {@code DelegatingFilterProxy} definition,
* with the specified {@code filter-name} corresponding to a bean name in
* Spring's root application context. All calls to the filter proxy will then
* be delegated to that bean in the Spring context, which is required to implement
* the standard Servlet Filter interface.
*
* <p>This approach is particularly useful for Filter implementation with complex
* setup needs, allowing to apply the full Spring bean definition machinery to
* Filter instances. Alternatively, consider standard Filter setup in combination
* with looking up service beans from the Spring root application context.
*
* <p><b>NOTE:</b> The lifecycle methods defined by the Servlet Filter interface
* will by default <i>not</i> be delegated to the target bean, relying on the
* Spring application context to manage the lifecycle of that bean. Specifying
* the "targetFilterLifecycle" filter init-param as "true" will enforce invocation
* of the {@code Filter.init} and {@code Filter.destroy} lifecycle methods
* on the target bean, letting the servlet container manage the filter lifecycle.
*
* <p>As of Spring 3.1, {@code DelegatingFilterProxy} has been updated to optionally accept
* constructor parameters when using Servlet 3.0's instance-based filter registration
* methods, usually in conjunction with Spring 3.1's
* {@link org.springframework.web.WebApplicationInitializer} SPI. These constructors allow
* for providing the delegate Filter bean directly, or providing the application context
* and bean name to fetch, avoiding the need to look up the application context from the
* ServletContext.
*
* <p>This class was originally inspired by Spring Security's {@code FilterToBeanProxy}
* class, written by Ben Alex.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @author Chris Beams
* @since 1.2
* @see #setTargetBeanName
* @see #setTargetFilterLifecycle
* @see javax.servlet.Filter#doFilter
* @see javax.servlet.Filter#init
* @see javax.servlet.Filter#destroy
* @see #DelegatingFilterProxy(Filter)
* @see #DelegatingFilterProxy(String)
* @see #DelegatingFilterProxy(String, WebApplicationContext)
* @see javax.servlet.ServletContext#addFilter(String, Filter)
* @see org.springframework.web.WebApplicationInitializer
*/
public class DelegatingFilterProxy extends GenericFilterBean {
private String contextAttribute;
private WebApplicationContext webApplicationContext;
private String targetBeanName;
private boolean targetFilterLifecycle = false;
private volatile Filter delegate;
private final Object delegateMonitor = new Object();
/**
* Create a new {@code DelegatingFilterProxy}. For traditional (pre-Servlet 3.0) use
* in {@code web.xml}.
* @see #setTargetBeanName(String)
*/
public DelegatingFilterProxy() {
}
/**
* Create a new {@code DelegatingFilterProxy} with the given {@link Filter} delegate.
* Bypasses entirely the need for interacting with a Spring application context,
* specifying the {@linkplain #setTargetBeanName target bean name}, etc.
* <p>For use in Servlet 3.0+ environments where instance-based registration of
* filters is supported.
* @param delegate the {@code Filter} instance that this proxy will delegate to and
* manage the lifecycle for (must not be {@code null}).
* @see #doFilter(ServletRequest, ServletResponse, FilterChain)
* @see #invokeDelegate(Filter, ServletRequest, ServletResponse, FilterChain)
* @see #destroy()
* @see #setEnvironment(org.springframework.core.env.Environment)
*/
public DelegatingFilterProxy(Filter delegate) {
Assert.notNull(delegate, "delegate Filter object must not be null");
this.delegate = delegate;
}
/**
* Create a new {@code DelegatingFilterProxy} that will retrieve the named target
* bean from the Spring {@code WebApplicationContext} found in the {@code ServletContext}
* (either the 'root' application context or the context named by
* {@link #setContextAttribute}).
* <p>For use in Servlet 3.0+ environments where instance-based registration of
* filters is supported.
* <p>The target bean must implement the standard Servlet Filter.
* @param targetBeanName name of the target filter bean to look up in the Spring
* application context (must not be {@code null}).
* @see #findWebApplicationContext()
* @see #setEnvironment(org.springframework.core.env.Environment)
*/
public DelegatingFilterProxy(String targetBeanName) {
this(targetBeanName, null);
}
/**
* Create a new {@code DelegatingFilterProxy} that will retrieve the named target
* bean from the given Spring {@code WebApplicationContext}.
* <p>For use in Servlet 3.0+ environments where instance-based registration of
* filters is supported.
* <p>The target bean must implement the standard Servlet Filter interface.
* <p>The given {@code WebApplicationContext} may or may not be refreshed when passed
* in. If it has not, and if the context implements {@link ConfigurableApplicationContext},
* a {@link ConfigurableApplicationContext#refresh() refresh()} will be attempted before
* retrieving the named target bean.
* <p>This proxy's {@code Environment} will be inherited from the given
* {@code WebApplicationContext}.
* @param targetBeanName name of the target filter bean in the Spring application
* context (must not be {@code null}).
* @param wac the application context from which the target filter will be retrieved;
* if {@code null}, an application context will be looked up from {@code ServletContext}
* as a fallback.
* @see #findWebApplicationContext()
* @see #setEnvironment(org.springframework.core.env.Environment)
*/
public DelegatingFilterProxy(String targetBeanName, WebApplicationContext wac) {
Assert.hasText(targetBeanName, "target Filter bean name must not be null or empty");
this.setTargetBeanName(targetBeanName);
this.webApplicationContext = wac;
if (wac != null) {
this.setEnvironment(wac.getEnvironment());
}
}
/**
* Set the name of the ServletContext attribute which should be used to retrieve the
* {@link WebApplicationContext} from which to load the delegate {@link Filter} bean.
*/
public void setContextAttribute(String contextAttribute) {
this.contextAttribute = contextAttribute;
}
/**
* Return the name of the ServletContext attribute which should be used to retrieve the
* {@link WebApplicationContext} from which to load the delegate {@link Filter} bean.
*/
public String getContextAttribute() {
return this.contextAttribute;
}
/**
* Set the name of the target bean in the Spring application context.
* The target bean must implement the standard Servlet Filter interface.
* <p>By default, the {@code filter-name} as specified for the
* DelegatingFilterProxy in {@code web.xml} will be used.
*/
public void setTargetBeanName(String targetBeanName) {
this.targetBeanName = targetBeanName;
}
/**
* Return the name of the target bean in the Spring application context.
*/
protected String getTargetBeanName() {
return this.targetBeanName;
}
/**
* Set whether to invoke the {@code Filter.init} and
* {@code Filter.destroy} lifecycle methods on the target bean.
* <p>Default is "false"; target beans usually rely on the Spring application
* context for managing their lifecycle. Setting this flag to "true" means
* that the servlet container will control the lifecycle of the target
* Filter, with this proxy delegating the corresponding calls.
*/
public void setTargetFilterLifecycle(boolean targetFilterLifecycle) {
this.targetFilterLifecycle = targetFilterLifecycle;
}
/**
* Return whether to invoke the {@code Filter.init} and
* {@code Filter.destroy} lifecycle methods on the target bean.
*/
protected boolean isTargetFilterLifecycle() {
return this.targetFilterLifecycle;
}
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
@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) {
if (this.delegate == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
this.delegate = initDelegate(wac);
}
delegateToUse = this.delegate;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
@Override
public void destroy() {
Filter delegateToUse = this.delegate;
if (delegateToUse != null) {
destroyDelegate(delegateToUse);
}
}
/**
* Return the {@code WebApplicationContext} passed in at construction time, if available.
* Otherwise, attempt to retrieve a {@code WebApplicationContext} from the
* {@code ServletContext} attribute with the {@linkplain #setContextAttribute
* configured name} if set. Otherwise look up a {@code WebApplicationContext} under
* the well-known "root" application context attribute. The
* {@code WebApplicationContext} must have already been loaded and stored in the
* {@code ServletContext} before this filter gets initialized (or invoked).
* <p>Subclasses may override this method to provide a different
* {@code WebApplicationContext} retrieval strategy.
* @return the {@code WebApplicationContext} for this proxy, or {@code null} if not found
* @see #DelegatingFilterProxy(String, WebApplicationContext)
* @see #getContextAttribute()
* @see WebApplicationContextUtils#getWebApplicationContext(javax.servlet.ServletContext)
* @see WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
*/
protected WebApplicationContext findWebApplicationContext() {
if (this.webApplicationContext != null) {
// The user has injected a context at construction time -> use it...
if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
ConfigurableApplicationContext cac = (ConfigurableApplicationContext) this.webApplicationContext;
if (!cac.isActive()) {
// The context has not yet been refreshed -> do so before returning it...
cac.refresh();
}
}
return this.webApplicationContext;
}
String attrName = getContextAttribute();
if (attrName != null) {
return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
}
else {
return WebApplicationContextUtils.findWebApplicationContext(getServletContext());
}
}
/**
* Initialize the Filter delegate, defined as bean the given Spring
* application context.
* <p>The default implementation fetches the bean from the application context
* and calls the standard {@code Filter.init} method on it, passing
* in the FilterConfig of this Filter proxy.
* @param wac the root application context
* @return the initialized delegate Filter
* @throws ServletException if thrown by the Filter
* @see #getTargetBeanName()
* @see #isTargetFilterLifecycle()
* @see #getFilterConfig()
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
/**
* Actually invoke the delegate Filter with the given request and response.
* @param delegate the delegate Filter
* @param request the current HTTP request
* @param response the current HTTP response
* @param filterChain the current FilterChain
* @throws ServletException if thrown by the Filter
* @throws IOException if thrown by the Filter
*/
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
/**
* Destroy the Filter delegate.
* Default implementation simply calls {@code Filter.destroy} on it.
* @param delegate the Filter delegate (never {@code null})
* @see #isTargetFilterLifecycle()
* @see javax.servlet.Filter#destroy()
*/
protected void destroyDelegate(Filter delegate) {
if (isTargetFilterLifecycle()) {
delegate.destroy();
}
}
}
DelegatingFilterProxy初始化并生成了FilterChainProxy实例,把所有的拦截请求都交给了FilterChainProxy进行处理。
FilterChainProxy
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
/**
* Delegates {@code Filter} requests to a list of Spring-managed filter beans. As of
* version 2.0, you shouldn't need to explicitly configure a {@code FilterChainProxy} bean
* in your application context unless you need very fine control over the filter chain
* contents. Most cases should be adequately covered by the default
* {@code <security:http />} namespace configuration options.
* <p>
* The {@code FilterChainProxy} is linked into the servlet container filter chain by
* adding a standard Spring {@link DelegatingFilterProxy} declaration in the application
* {@code web.xml} file.
*
* <h2>Configuration</h2>
* <p>
* As of version 3.1, {@code FilterChainProxy} is configured using a list of
* {@link SecurityFilterChain} instances, each of which contains a {@link RequestMatcher}
* and a list of filters which should be applied to matching requests. Most applications
* will only contain a single filter chain, and if you are using the namespace, you don't
* have to set the chains explicitly. If you require finer-grained control, you can make
* use of the {@code <filter-chain>} namespace element. This defines a URI pattern
* and the list of filters (as comma-separated bean names) which should be applied to
* requests which match the pattern. An example configuration might look like this:
*
* <pre>
* <bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
* <constructor-arg>
* <util:list>
* <security:filter-chain pattern="/do/not/filter*" filters="none"/>
* <security:filter-chain pattern="/**" filters="filter1,filter2,filter3"/>
* </util:list>
* </constructor-arg>
* </bean>
* </pre>
*
* The names "filter1", "filter2", "filter3" should be the bean names of {@code Filter}
* instances defined in the application context. The order of the names defines the order
* in which the filters will be applied. As shown above, use of the value "none" for the
* "filters" can be used to exclude a request pattern from the security filter chain
* entirely. Please consult the security namespace schema file for a full list of
* available configuration options.
*
* <h2>Request Handling</h2>
* <p>
* Each possible pattern that the {@code FilterChainProxy} should service must be entered.
* The first match for a given request will be used to define all of the {@code Filter}s
* that apply to that request. This means you must put most specific matches at the top of
* the list, and ensure all {@code Filter}s that should apply for a given matcher are
* entered against the respective entry. The {@code FilterChainProxy} will not iterate
* through the remainder of the map entries to locate additional {@code Filter}s.
* <p>
* {@code FilterChainProxy} respects normal handling of {@code Filter}s that elect not to
* call
* {@link javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)}
* , in that the remainder of the original or {@code FilterChainProxy}-declared filter
* chain will not be called.
*
* <h3>Request Firewalling</h3>
*
* An {@link HttpFirewall} instance is used to validate incoming requests and create a
* wrapped request which provides consistent path values for matching against. See
* {@link DefaultHttpFirewall}, for more information on the type of attacks which the
* default implementation protects against. A custom implementation can be injected to
* provide stricter control over the request contents or if an application needs to
* support certain types of request which are rejected by default.
* <p>
* Note that this means that you must use the Spring Security filters in combination with
* a {@code FilterChainProxy} if you want this protection. Don't define them explicitly in
* your {@code web.xml} file.
* <p>
* {@code FilterChainProxy} will use the firewall instance to obtain both request and
* response objects which will be fed down the filter chain, so it is also possible to use
* this functionality to control the functionality of the response. When the request has
* passed through the security filter chain, the {@code reset} method will be called. With
* the default implementation this means that the original values of {@code servletPath}
* and {@code pathInfo} will be returned thereafter, instead of the modified ones used for
* security pattern matching.
* <p>
* Since this additional wrapping functionality is performed by the
* {@code FilterChainProxy}, we don't recommend that you use multiple instances in the
* same filter chain. It shouldn't be considered purely as a utility for wrapping filter
* beans in a single {@code Filter} instance.
*
* <h2>Filter Lifecycle</h2>
* <p>
* Note the {@code Filter} lifecycle mismatch between the servlet container and IoC
* container. As described in the {@link DelegatingFilterProxy} Javadocs, we recommend you
* allow the IoC container to manage the lifecycle instead of the servlet container.
* {@code FilterChainProxy} does not invoke the standard filter lifecycle methods on any
* filter beans that you add to the application context.
*
* @author Carlos Sanchez
* @author Ben Alex
* @author Luke Taylor
* @author Rob Winch
*/
public class FilterChainProxy extends GenericFilterBean {
// ~ Static fields/initializers
// =====================================================================================
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
// ~ Instance fields
// ================================================================================================
private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
private HttpFirewall firewall = new DefaultHttpFirewall();
// ~ Methods
// ========================================================================================================
public FilterChainProxy() {
}
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}
@Override
public void afterPropertiesSet() {
filterChainValidator.validate(this);
}
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, null)
.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);
}
/**
* Used (internally) to specify a validation strategy for the filters in each
* configured chain.
*
* @param filterChainValidator the validator instance which will be invoked on during
* initialization to check the {@code FilterChainProxy} instance.
*/
public void setFilterChainValidator(FilterChainValidator filterChainValidator) {
this.filterChainValidator = filterChainValidator;
}
/**
* Sets the "firewall" implementation which will be used to validate and wrap (or
* potentially reject) the incoming requests. The default implementation should be
* satisfactory for most requirements.
*
* @param firewall
*/
public void setFirewall(HttpFirewall firewall) {
this.firewall = firewall;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("FilterChainProxy[");
sb.append("Filter Chains: ");
sb.append(filterChains);
sb.append("]");
return sb.toString();
}
// ~ Inner Classes
// ==================================================================================================
/**
* Internal {@code FilterChain} implementation that is used to pass a request through
* the additional internal list of filters which match the request.
*/
private static class VirtualFilterChain implements FilterChain {
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;
}
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);
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);
}
}
}
public interface FilterChainValidator {
void validate(FilterChainProxy filterChainProxy);
}
private static class NullFilterChainValidator implements FilterChainValidator {
public void validate(FilterChainProxy filterChainProxy) {
}
}
}
在执行doFilterInternal时,会通过该请求进行匹配,获取该请求需要匹配的Filters
/**
* 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;
}
此处会匹配到12个Filter,如图所示。
核心的是OAuth2ClientAuthenticationProcessingFilter
then execute the follows:
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);
VirtualFilterChain
// ~ Inner Classes
// ==================================================================================================
/**
* Internal {@code FilterChain} implementation that is used to pass a request through
* the additional internal list of filters which match the request.
*/
private static class VirtualFilterChain implements FilterChain {
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;
}
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);
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也相当于是一个过滤器链调用,implements FilterChain,和ApplicationFilterChain类似。执行Filter链式调用。
当执行到OAuth2ClientAuthenticationProcessingFilter时,会执行父类AbstractAuthenticationProcessingFilter类的doFilter method,because:
OAuth2ClientAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter doFilter method:
/**
* Invokes the
* {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
* requiresAuthentication} method to determine whether the request is for
* authentication and should be handled by this filter. If it is an authentication
* request, the
* {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
* attemptAuthentication} will be invoked to perform the authentication. There are
* then three possible outcomes:
* <ol>
* <li>An <tt>Authentication</tt> object is returned. The configured
* {@link SessionAuthenticationStrategy} will be invoked (to handle any
* session-related behaviour such as creating a new session to protect against
* session-fixation attacks) followed by the invocation of
* {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
* method</li>
* <li>An <tt>AuthenticationException</tt> occurs during authentication. The
* {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
* unsuccessfulAuthentication} method will be invoked</li>
* <li>Null is returned, indicating that the authentication process is incomplete. The
* method will then return immediately, assuming that the subclass has done any
* necessary work (such as redirects) to continue the authentication process. The
* assumption is that a later request will be received by this method where the
* returned <tt>Authentication</tt> object is not null.
* </ol>
*/
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);
}
然后会依次执行VirtualFilterChain的filters链的下一个nextFilter。
当执行到AnonymousAuthenticationFilter时,会调用createAuthentication method,创建一个AnonymousAuthenticationToken,并且设置到
SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));
然后依次执行VirtualFilterChain的filters链的下一个nextFilter。
执行到FilterSecurityInterceptor时,控制台会打印如下内容:
2017-03-30 10:35:14.573 DEBUG 13068 --- [nio-8080-exec-6] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /time; Attributes: [authenticated]
2017-03-30 10:35:14.574 DEBUG 13068 --- [nio-8080-exec-6] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9057bc48: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@2cd90: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: CCCACD542EB53EE17657F4BCDCD6EBD3; Granted Authorities: ROLE_ANONYMOUS
2017-03-30 10:35:14.575 DEBUG 13068 --- [nio-8080-exec-6] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@65e59acf, returned: -1
并将异常抛出到ExceptionTranslationFilter过滤器,Because FilterSecurityInterceptor过滤器是在ExceptionTranslationFilter中执行的doFilter。进入ExceptionTranslationFilter的catch代码块:
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
获取抛出的根异常信息,当没有登录时,此时抛出的异常为AccessDeniedException。
紧接着执行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) {
if (authenticationTrustResolver.isAnonymous(SecurityContextHolder
.getContext().getAuthentication())) {
logger.debug(
"Access is denied (user is anonymous); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"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);
}
}
}
because user is anonymous,redirecting to authentication entry endpoint.
进入second else if code
else if (exception instanceof AccessDeniedException) {
if (authenticationTrustResolver.isAnonymous(SecurityContextHolder
.getContext().getAuthentication())) {
logger.debug(
"Access is denied (user is anonymous); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
sendStartAuthentication method
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
清空掉SecurityContextHolder.getContext(0.setAuthentication(null);也就是清除之前创建的anonymousAuthentication。
紧接着调用requestCache.saveRequest(request,response);
saveReuqest
/**
* Stores the current request, provided the configuration properties allow it.
*/
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
if (requestMatcher.matches(request)) {
DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
portResolver);
if (createSessionAllowed || request.getSession(false) != null) {
// Store the HTTP request itself. Used by
// AbstractAuthenticationProcessingFilter
// for redirection after successful authentication (SEC-29)
request.getSession().setAttribute(SAVED_REQUEST, savedRequest);
logger.debug("DefaultSavedRequest added to Session: " + savedRequest);
}
}
else {
logger.debug("Request not saved as configured RequestMatcher did not match");
}
}
Create Session,并且保存当前请求的内容。例如:第一次访问http://localhost:8080/time的请求内容。此处savedRequest内容如图,存放到Session中。Session的Key值为:SPRING_SECURITY_SAVED_REQUEST
然后执行authenticationEntryPoint.commence(request, response, reason);代码。
此处的authenticationEntryPoint为
org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint。
代码如下:
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
for (RequestMatcher requestMatcher : entryPoints.keySet()) {
if (logger.isDebugEnabled()) {
logger.debug("Trying to match using " + requestMatcher);
}
if (requestMatcher.matches(request)) {
AuthenticationEntryPoint entryPoint = entryPoints.get(requestMatcher);
if (logger.isDebugEnabled()) {
logger.debug("Match found! Executing " + entryPoint);
}
entryPoint.commence(request, response, authException);
return;
}
}
if (logger.isDebugEnabled()) {
logger.debug("No match found. Using default entry point " + defaultEntryPoint);
}
// No EntryPoint matched, use defaultEntryPoint
defaultEntryPoint.commence(request, response, authException);
}
在依次判断URL是否符合配置的拦截。相当于在执行继承WebSecurityAdapter重写configure的配置。
紧接着控制台显示:
2017-03-30 10:41:45.162 DEBUG 13068 --- [nio-8080-exec-6] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@4d5b55cf
匹配到LoginUrlAuthenticationEntryPoint的commence的代码块
// redirect to login page. Use https if forceHttps true
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
buildRedirectUrlToLoginPage,表示跳转到登陆页面。方法会生成一个跳转的URL地址。redirectUrl为:http://localhost:8080/login。
sendRedirect代码块:执行跳转
public void sendRedirect(HttpServletRequest request, HttpServletResponse response,
String url) throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (logger.isDebugEnabled()) {
logger.debug("Redirecting to '" + redirectUrl + "'");
}
response.sendRedirect(redirectUrl);
}
sendRedirect的路径为:http://localhost:8080/login
然后依次回退到ExceptionTranslationFilter的handleSpringSecurityException,后退到FilterChainProxy,退到SessionManagementFilter,退到VirtualFilterChain的doFilter。
退到AnonymousAuthenticationFilter,退到FilterChainProxy,退到SecurityContextHolderAwareReuqestFilter。
退到FilterChainProxy,退到RequestCacheAwareFilter,退到FilterChainProxy,退到AbstractAuthenticationProcessingFilter的doFilter的代码块的此处:
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;
}
此处会进行return;,然后退到FilterChainProxy,退到LogoutFilter,退到FilterChainProxy,csrfFilter的代码块的此处:
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
CsrfToken generatedToken = tokenRepository.generateToken(request);
csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response,
generatedToken);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
此处会进入return;退到FilterChainProxy,退到HeaderWriterFilter。退到SecurityContextPersistenceFilter的Finally代码块:
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
此处contextAfterChainExecution为org.springframework.security.core.context.SecurityContextImpl@ffffffff: Null authentication
退到退到FilterChainProxy,退到WebAsyncManagerIntegrationFilter,退到FilterChainProxy的如下代码处:
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);
}
直到退到FilterChainProxy的doFilter的代码块:
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);
}
}
FilterChainProxy的finally处。
因为上面的代码执行了response.sendRedirect,所以会重定向到http://localhost:8080/login,同时会在Session中保存实际上访问的真实请求的内容。
所以第二次请求进来时会进行判断,直到到AbstractAuthenticationProcessFilter的doFilter代码块处。会执行如下代码:
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);
}
此时如下判断将不会再次进入
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
而是继续执行以下代码,开始调用:OAuth2ClientAuthenticationProcessingFilter的attemptAuthentication方法,如下:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
return result;
}
catch (InvalidTokenException e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
restTemplate为OAuth2RestTemplate,类似于HttpClient。通过restTemplate.getAccessToken代码:
/**
* Acquire or renew an access token for the current context if necessary. This method will be called automatically
* when a request is executed (and the result is cached), but can also be called as a standalone method to
* pre-populate the token.
*
* @return an access token
*/
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
OAuth2AccessToken accessToken = context.getAccessToken();
if (accessToken == null || accessToken.isExpired()) {
try {
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
context.setAccessToken(null); // No point hanging onto it now
accessToken = null;
String stateKey = e.getStateKey();
if (stateKey != null) {
Object stateToPreserve = e.getStateToPreserve();
if (stateToPreserve == null) {
stateToPreserve = "NONE";
}
context.setPreservedState(stateKey, stateToPreserve);
}
throw e;
}
}
return accessToken;
}
当从context中getAccessToken()会返回空。context为org.springframework.security.oauth2.client.DefaultOAuth2ClientContext@2fec955c,针对Session每次生成一个该对象。
发现为null的话会进入acquireAccessToken代码块:
protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context)
throws UserRedirectRequiredException {
AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest();
if (accessTokenRequest == null) {
throw new AccessTokenRequiredException(
"No OAuth 2 security context has been established. Unable to access resource '"
+ this.resource.getId() + "'.", resource);
}
// Transfer the preserved state from the (longer lived) context to the current request.
String stateKey = accessTokenRequest.getStateKey();
if (stateKey != null) {
accessTokenRequest.setPreservedState(oauth2Context.removePreservedState(stateKey));
}
OAuth2AccessToken existingToken = oauth2Context.getAccessToken();
if (existingToken != null) {
accessTokenRequest.setExistingToken(existingToken);
}
OAuth2AccessToken accessToken = null;
accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
if (accessToken == null || accessToken.getValue() == null) {
throw new IllegalStateException(
"Access token provider returned a null access token, which is illegal according to the contract.");
}
oauth2Context.setAccessToken(accessToken);
return accessToken;
}
此时还未生成stateKey,并且OAuth2AccessToken existingToken = oauth2Context.getAccessToken();
并且existingToken也为空。。。。
执行以下代码块:
OAuth2AccessToken accessToken = null;
accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
if (accessToken == null || accessToken.getValue() == null) {
throw new IllegalStateException(
"Access token provider returned a null access token, which is illegal according to the contract.");
}
oauth2Context.setAccessToken(accessToken);
return accessToken;
此处的accessTokenProvider为org.springframework.security.oauth2.client.token.AccessTokenProviderChain@452c7964
进入obtainAccessToken方法。实际上是进入AuthenticationCodeAccessProvider。AccessTokenProviderChain只是一个模板类,用来通过他所有的子类的obtainAccessToken方法进行获取。
实际上调用的AuthenticationCodeAccessProvider的obtainAccessToken。执行以下代码:
return tokenProvider.obtainAccessToken(details, request);
details为:AuthorizationCodeResourceDetails,
以下是AuthenticationCodeAccessProvider.obtainAccessToken的代码块:
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
getHeadersForTokenRequest(request));
}
此处发现rqeuest.getAuthorizationCode() ==null并且request.getStateKey也等于空。
就会抛出getRedirectForAuthorization异常。进入getRedirectForAuthorization的代码块:
private UserRedirectRequiredException getRedirectForAuthorization(AuthorizationCodeResourceDetails resource,
AccessTokenRequest request) {
// we don't have an authorization code yet. So first get that.
TreeMap<String, String> requestParameters = new TreeMap<String, String>();
requestParameters.put("response_type", "code"); // oauth2 spec, section 3
requestParameters.put("client_id", resource.getClientId());
// Client secret is not required in the initial authorization request
String redirectUri = resource.getRedirectUri(request);
if (redirectUri != null) {
requestParameters.put("redirect_uri", redirectUri);
}
if (resource.isScoped()) {
StringBuilder builder = new StringBuilder();
List<String> scope = resource.getScope();
if (scope != null) {
Iterator<String> scopeIt = scope.iterator();
while (scopeIt.hasNext()) {
builder.append(scopeIt.next());
if (scopeIt.hasNext()) {
builder.append(' ');
}
}
}
requestParameters.put("scope", builder.toString());
}
UserRedirectRequiredException redirectException = new UserRedirectRequiredException(
resource.getUserAuthorizationUri(), requestParameters);
String stateKey = stateKeyGenerator.generateKey(resource);
redirectException.setStateKey(stateKey);
request.setStateKey(stateKey);
redirectException.setStateToPreserve(redirectUri);
request.setPreservedState(redirectUri);
return redirectException;
}
此时会封装需要返回的页面,也就是发送授权请求的redirectUri:http://localhost:8080/login
并且生成stateKey,因为抛出异常会回退到以下代码块的catch处:
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
OAuth2AccessToken accessToken = context.getAccessToken();
if (accessToken == null || accessToken.isExpired()) {
try {
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
context.setAccessToken(null); // No point hanging onto it now
accessToken = null;
String stateKey = e.getStateKey();
if (stateKey != null) {
Object stateToPreserve = e.getStateToPreserve();
if (stateToPreserve == null) {
stateToPreserve = "NONE";
}
context.setPreservedState(stateKey, stateToPreserve);
}
throw e;
}
}
return accessToken;
}
并且将stateKey和redirectUri(http://localhost:8080/login)存储到DefaultOAuth2ClientContext中(相当于Session)作用域。然后继续上抛异常到OAuth2RestTemplate,最终回退到OAuth2ClientContextFilter的doFilter代码块的catch处:
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
request.setAttribute(CURRENT_URI, calculateCurrentUri(request));
try {
chain.doFilter(servletRequest, servletResponse);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
.getFirstThrowableOfType(
UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
redirectUser(redirect, request, response);
} else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new NestedServletException("Unhandled exception", ex);
}
}
}
查找发现第一个抛出的异常的类型为UserRedirectRequiredException,表示需要执行redirect。所以会进入redirectUser(redirect, request, response);代码块:
如下代码块:
/**
* Redirect the user according to the specified exception.
*
* @param e
* The user redirect exception.
* @param request
* The request.
* @param response
* The response.
*/
protected void redirectUser(UserRedirectRequiredException e,
HttpServletRequest request, HttpServletResponse response)
throws IOException {
String redirectUri = e.getRedirectUri();
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(redirectUri);
Map<String, String> requestParams = e.getRequestParams();
for (Map.Entry<String, String> param : requestParams.entrySet()) {
builder.queryParam(param.getKey(), param.getValue());
}
if (e.getStateKey() != null) {
builder.queryParam("state", e.getStateKey());
}
this.redirectStrategy.sendRedirect(request, response, builder.build()
.encode().toUriString());
}
UserRedirectRequireException异常如图:
此处的redirectUri为http://localhost:9090/auth/oauth/authorize,是授权服务器的地址,在appplication.xml中配置。
requestParameters为下图内容:
并且添加stateKey,进入sendRedirect方法,redirectUri:
http://localhost:9090/auth/oauth/authorize?client_id=myauthserver&redirect_uri=http://localhost:8080/login&response_type=code&state=wgvXPS
sendRedirect如下代码块:
public void sendRedirect(HttpServletRequest request, HttpServletResponse response,
String url) throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (logger.isDebugEnabled()) {
logger.debug("Redirecting to '" + redirectUrl + "'");
}
response.sendRedirect(redirectUrl);
}
执行response.sendRedirect(redirectUrl);会跳转到这个地址。
此时会跳转到授权服务器的登陆页面,同时Fiddler的内容如下:
授权服务器会跳转到/login,所以authorize的状态为302。同时Fiddler会有一条请求:
登陆页面如下:
登陆成功后,授权服务器会向该地址进行请求。
进入ApplicationFilterChain的internalDoFilter的如下代码块,继续循环:
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = null;
try {
filter = filterConfig.getFilter();
support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
filter, request, response);
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege
("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response);
} catch (IOException | ServletException | RuntimeException e) {
if (filter != null)
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response, e);
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
if (filter != null)
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
filter, request, response, e);
throw new ServletException
(sm.getString("filterChain.filter"), e);
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
support.fireInstanceEvent(InstanceEvent.BEFORE_SERVICE_EVENT,
servlet, request, response);
if (request.isAsyncSupported()
&& !support.getWrapper().isAsyncSupported()) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse)) {
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
servlet.service(request, response);
}
} else {
servlet.service(request, response);
}
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response);
} catch (IOException e) {
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw e;
} catch (ServletException e) {
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw e;
} catch (RuntimeException e) {
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
servlet, request, response, e);
throw new ServletException
(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}
每当有请求时,都会执行该过滤器链。
再次进入到OAuth2ClientContextFilter过滤器时,此时的current_uri为:currentUri=http://localhost:8080/login?state=wgvXPS
进入到SecurityCotnextFilter时,会生成VirtualFilterChain,Security的安全过滤器链,一共12个过滤器。
来回在VirtualFilterChain中执行这12个过滤器。直到进入OAuth2ClientAuthenticationProcessingFilter的attemptAuthentication代码块:
再次进入AuthorizationCodeAccessTokenProvider的obtainAccessToken代码块:
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
getHeadersForTokenRequest(request));
}
再次执行request.getAuthorizationCode() 和request.getStateKey()时数据不为空,所以会进入retrieveToken方法。
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
try {
// Prepare headers and form before going into rest template call in case the URI is affected by the result
authenticationHandler.authenticateTokenRequest(resource, form, headers);
// Opportunity to customize form and headers
tokenRequestEnhancer.enhance(request, resource, form, headers);
final AccessTokenRequest copy = request;
final ResponseExtractor<OAuth2AccessToken> delegate = getResponseExtractor();
ResponseExtractor<OAuth2AccessToken> extractor = new ResponseExtractor<OAuth2AccessToken>() {
@Override
public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
if (response.getHeaders().containsKey("Set-Cookie")) {
copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
}
return delegate.extractData(response);
}
};
return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),
getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());
}
catch (OAuth2Exception oe) {
throw new OAuth2AccessDeniedException("Access token denied.", resource, oe);
}
catch (RestClientException rce) {
throw new OAuth2AccessDeniedException("Error requesting access token.", resource, rce);
}
}
先执行以下方法:
// Prepare headers and form before going into rest template call in case the URI is affected by the result
authenticationHandler.authenticateTokenRequest(resource, form, headers);
authenticateTokenRequest方法如下:
public void authenticateTokenRequest(OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form,
HttpHeaders headers) {
if (resource.isAuthenticationRequired()) {
AuthenticationScheme scheme = AuthenticationScheme.header;
if (resource.getClientAuthenticationScheme() != null) {
scheme = resource.getClientAuthenticationScheme();
}
try {
String clientSecret = resource.getClientSecret();
clientSecret = clientSecret == null ? "" : clientSecret;
switch (scheme) {
case header:
form.remove("client_secret");
headers.add(
"Authorization",
String.format(
"Basic %s",
new String(Base64.encode(String.format("%s:%s", resource.getClientId(),
clientSecret).getBytes("UTF-8")), "UTF-8")));
break;
case form:
case query:
form.set("client_id", resource.getClientId());
if (StringUtils.hasText(clientSecret)) {
form.set("client_secret", clientSecret);
}
break;
default:
throw new IllegalStateException(
"Default authentication handler doesn't know how to handle scheme: " + scheme);
}
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
}
此处的scheme为Header,添加头请求:Authorization Basic 并且将ClientId和client_secret使用Base64.encode进行转换。
form.remove("client_secret");
headers.add(
"Authorization",
String.format(
"Basic %s",
new String(Base64.encode(String.format("%s:%s", resource.getClientId(),
clientSecret).getBytes("UTF-8")), "UTF-8")));
紧接着执行
// Opportunity to customize form and headers
tokenRequestEnhancer.enhance(request, resource, form, headers);
此时的request中已经包含了code和stateKey。
再执行以下代码:
final ResponseExtractor<OAuth2AccessToken> delegate = getResponseExtractor();
ResponseExtractor<OAuth2AccessToken> extractor = new ResponseExtractor<OAuth2AccessToken>() {
@Override
public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
if (response.getHeaders().containsKey("Set-Cookie")) {
copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
}
return delegate.extractData(response);
}
};
return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),
getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());
resource的内容如图,将向资源服务器发送请求,http://localhost:9090/auth/oauth/token
获取access_token。进行调用后会return到AccessToken到AccessTokenProviderChain(模板类),获取Token的模板。
AccessTokenProviderChain.obtainAccessToken的方法,如下代码块出:
if (accessToken == null) {
// looks like we need to try to obtain a new token.
accessToken = obtainNewAccessTokenInternal(resource, request);
if (accessToken == null) {
throw new IllegalStateException("An OAuth 2 access token must be obtained or an exception thrown.");
}
}
if (clientTokenServices != null && (resource.isClientOnly() || auth != null && auth.isAuthenticated())) {
clientTokenServices.saveAccessToken(resource, auth, accessToken);
}
此时如果clientTokenService不为空的话,会保存saveAccessToken。
回退到OAuth2RestTemplate的acquireAccessToken方法的如下代码块处:
OAuth2AccessToken accessToken = null;
accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
if (accessToken == null || accessToken.getValue() == null) {
throw new IllegalStateException(
"Access token provider returned a null access token, which is illegal according to the contract.");
}
oauth2Context.setAccessToken(accessToken);
在oauth2Context(DefaultOAuth2ClientContext)Session作用及的对象中保存accessToken的值。
并返回到OAuth2RestTemplate的getAccessToken处:
if (accessToken == null || accessToken.isExpired()) {
try {
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
context.setAccessToken(null); // No point hanging onto it now
accessToken = null;
String stateKey = e.getStateKey();
if (stateKey != null) {
Object stateToPreserve = e.getStateToPreserve();
if (stateToPreserve == null) {
stateToPreserve = "NONE";
}
context.setPreservedState(stateKey, stateToPreserve);
}
throw e;
}
}
return accessToken;
}
然后回到restTemplate.getAccessToken的调用处,OAuth2ClientAuthenticationProcessingFilter的attemptAuthentication method
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
return result;
}
catch (InvalidTokenException e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
tokenServices为:UserInfoTokenServices。loadAuthentication method:
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
this.logger.debug("userinfo returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
调用getMap()方法:
private Map<String, Object> getMap(String path, String accessToken) {
this.logger.info("Getting user info from: " + path);
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
.getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
accessToken);
token.setTokenType(this.tokenType);
restTemplate.getOAuth2ClientContext().setAccessToken(token);
}
return restTemplate.getForEntity(path, Map.class).getBody();
}
catch (Exception ex) {
this.logger.info("Could not fetch user details: " + ex.getClass() + ", "
+ ex.getMessage());
return Collections.<String, Object>singletonMap("error",
"Could not fetch user details");
}
}
此处的path为http://localhost:9090/auth/user,通过带上client_id和之前的access_token值进行访问。
此时会经过RestTemplate访问http://localhost:9090/auth/user,所有请求头和内容都会进行封装。
调用后回到UserInfoTokenServices的loadAuthentication方法。
extractAuthentication的代码块:
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
List<GrantedAuthority> authorities = this.authoritiesExtractor
.extractAuthorities(map);
OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
null, null, null, null);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
然后新建OAuth2Authentication,返回到OAuth2ClientAuthenticationProcessingFilter的attemptAuthentication的如下代码块:
try {
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
return result;
}
在request中设置attribute,
先设置OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE和accessToken.getValue
然后设置OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE和accessToekn.getTokenType
最后回退到AbstractAuthenticationProcessingFilter的如下代码块:
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);
}
此处的SessionStrategy为:CompositeSessionAuthenticationStrategy
最后调用AbstractAuthenticationProcessingFilter的successfulAuthentication方法。
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// Nearly a no-op, but if there is a ClientTokenServices then the token will now be stored
restTemplate.getAccessToken();
}
super.successfulAuthentication的代码块内容:
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the
* {@link SecurityContextHolder}</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* Subclasses can override this method to continue the {@link FilterChain} after
* successful authentication.
* @param request
* @param response
* @param chain
* @param authResult the object returned from the <tt>attemptAuthentication</tt>
* method.
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
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);
}
successHandler.onAuthenticationSuccess代码块内容如下:实际上进入SavedReuqestAwareAuthenticationSuccessHandler,代码如下:
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
会从rqeuestCach获取真实访问的请求,并重定向到真是访问请求的地址。
最后回到OAuth2ClientAuthenticationProcessingFilter的successfulAuthentication,代码块,再次执行:restTemplate.getAccessToken();
当执行完SecurityContextFilter所有的Filters安全链,一共12个时,会回退到VirtualFilterChain。(位于第6个Filter:ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.web.filter.DelegatingFilterProxy])
执行以下代码:
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);
}
每次请求都会经过SecurityContextFilter的12Filter安全链,日志如下:
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/css/**']
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/css/**'
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/js/**']
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/js/**'
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/images/**']
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/images/**'
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/**/favicon.ico']
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/**/favicon.ico'
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/error']
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/error'
2017-03-30 12:26:44.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.web.util.matcher.OrRequestMatcher : No matches found
2017-03-30 12:26:44.750 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2017-03-30 12:26:46.018 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2017-03-30 12:26:46.317 DEBUG 13068 --- [nio-8080-exec-8] w.c.HttpSessionSecurityContextRepository : Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'org.springframework.security.core.context.SecurityContextImpl@cb35e672: Authentication: org.springframework.security.oauth2.provider.OAuth2Authentication@cb35e672: Principal: admin; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=0:0:0:0:0:0:0:1, sessionId=<SESSION>, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_ADMIN, ROLE_USER'
2017-03-30 12:26:46.486 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2017-03-30 12:26:46.846 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@296f902
2017-03-30 12:26:47.102 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 4 of 12 in additional filter chain; firing Filter: 'CsrfFilter'
2017-03-30 12:26:47.600 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 5 of 12 in additional filter chain; firing Filter: 'LogoutFilter'
2017-03-30 12:26:47.909 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'GET /api/time' doesn't match 'POST /logout
2017-03-30 12:26:48.074 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
2017-03-30 12:26:48.413 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/login'
2017-03-30 12:26:48.573 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 7 of 12 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
2017-03-30 12:26:49.065 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 8 of 12 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
2017-03-30 12:26:49.555 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 9 of 12 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
2017-03-30 12:26:49.785 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.a.AnonymousAuthenticationFilter : SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.oauth2.provider.OAuth2Authentication@cb35e672: Principal: admin; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=0:0:0:0:0:0:0:1, sessionId=<SESSION>, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_ADMIN, ROLE_USER'
2017-03-30 12:26:50.037 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 10 of 12 in additional filter chain; firing Filter: 'SessionManagementFilter'
2017-03-30 12:26:50.703 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 11 of 12 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/time'; against '/'
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /api/time; Attributes: [authenticated]
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.oauth2.provider.OAuth2Authentication@cb35e672: Principal: admin; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=0:0:0:0:0:0:0:1, sessionId=<SESSION>, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_ADMIN, ROLE_USER
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@65e59acf, returned: 1
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.a.i.FilterSecurityInterceptor : Authorization successful
2017-03-30 12:26:51.092 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.a.i.FilterSecurityInterceptor : RunAsManager did not change Authentication object
2017-03-30 12:26:51.210 DEBUG 13068 --- [nio-8080-exec-8] o.s.security.web.FilterChainProxy : /api/time reached end of additional filter chain; proceeding with original chain
2017-03-30 12:26:56.548 DEBUG 13068 --- [nio-8080-exec-8] g.c.AuthorizationCodeAccessTokenProvider : Retrieving token from http://localhost:9090/auth/oauth/token
2017-03-30 12:26:56.549 DEBUG 13068 --- [nio-8080-exec-8] g.c.AuthorizationCodeAccessTokenProvider : Encoding and sending form: {grant_type=[refresh_token], refresh_token=[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbIm15c2NvcGUiXSwiYXRpIjoiODZiNzlmM2ItOWNiMi00MDY3LWIwMTctZTExNWE1ZjNkZDE3IiwiZXhwIjoxNDkwODQ4Njc4LCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sImp0aSI6IjE1Y2ExNWZmLTc3N2ItNGI5OS04ZTMzLTlkYzM4YjlhNmQxMiIsImNsaWVudF9pZCI6Im15YXV0aHNlcnZlciJ9.U_o0ybdZrs6_qzWnTP_WKeCPsTLXcihJe-dKWIIgL1dmebl9QPhUJcvrzBFOrg_e8byZsMRR9By7-GG7BcAtqX6ljfA0MyveUDwtGxKgGS7LfEjBu_tUegmmsLFcHc8s6G0sZyQsgQQ3qTtziNNztjpsLbRXp9q3sIYIHJiMh5Cz2U38ucTCJWHZtnaLYvG7c72E92EXlWfv5Ek672WCbkONYcVjumPHdFYMFBKARZbHRHwbwJI7hNRO5hXaFOkvwSQ-zYrbndlKsXNUmgBRQs7pdJBgZ77ogmPmxU0la6W4hLs-bwCS0le2tYpHi-iuhGvAvDMfhsFlbqKb0irj_Q]}
2017-03-30 12:26:59.121 DEBUG 13068 --- [nio-8080-exec-8] o.s.s.w.a.ExceptionTranslationFilter : Chain processed normally
2017-03-30 12:26:59.121 DEBUG 13068 --- [nio-8080-exec-8] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
过滤链非常的多,首先是ApplicationFilterChain的10个过滤链,然后是SecurityContextFilterChain的12个过滤器链,如果应用业务也增加了一些过滤器链将会相当的繁琐,并且一定要注意控制顺序。