Spring Cloud api-gateway using @EnableOAuth2Sso 註解。css
Start API-GATEWAY application。java
When request arrived,會進入ApplicationFilterChain的過濾器鏈。web
ApplicationFilterChain位於tomcat-embed-core.jar的一下路徑:spring
org.apache.catalina.core.ApplicationFilterChain.classexpress
默認會進入ApplicationFilterChain的internalDoFilter method。apache
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方法,以下所示:api
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以下所示:tomcat
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個過濾器鏈,若是應用業務也增長了一些過濾器鏈將會至關的繁瑣,而且必定要注意控制順序。