1. 程式人生 > >Spring Security Web 5.1.2 原始碼解析 -- HttpSessionSecurityContextRepository

Spring Security Web 5.1.2 原始碼解析 -- HttpSessionSecurityContextRepository

Spring Security Web提供的類HttpSessionSecurityContextRepository是一個SecurityContextRepository介面的實現,用於在HttpSession中儲存安全上下文(security context),這樣屬於同一個HttpSession的多個請求,就能夠利用此機制訪問同一安全上下文了。

package org.springframework.security.web.context;

import javax.servlet.AsyncContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; import org.springframework.
security.core.Transient; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.web.util.WebUtils; public class HttpSessionSecurityContextRepository implements SecurityContextRepository { /** * The default key under which the security context will be stored in the session. * 安全上下文在HttpSession中儲存時會儲存為HttpSession的一個屬性,這個字串是預設使用的屬性名稱 */ public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; protected final Log logger = LogFactory.getLog(this.getClass()); /** * SecurityContext instance used to check for equality with default (unauthenticated) * content * 預設情況,也就是未認證情況下檢查安全上下文相等時預設使用的安全上下文例項 */ private final Object contextObject = SecurityContextHolder.createEmptyContext(); private boolean allowSessionCreation = true; private boolean disableUrlRewriting = false; private boolean isServlet3 = ClassUtils.hasMethod(ServletRequest.class, "startAsync"); private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY; private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); /** * Gets the security context for the current request (if available) and returns it. * 獲取當前請求的安全上下文並返回 * * If the session is null, the context object is null or the context object stored in * the session is not an instance of SecurityContext, a new context object * will be generated and returned. * 如果當前請求對應的session為null,安全上下文物件為null,或者session中儲存的安全上下文物件 * 不是類SecurityContext的例項,建立一個信的安全上下文物件並返回 */ public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { HttpServletRequest request = requestResponseHolder.getRequest(); HttpServletResponse response = requestResponseHolder.getResponse(); // 獲取當前請求對應的session物件httpSession,注意這裡的引數是false,也就是說 // 如果當前請求對應的session物件為null並不建立新的session物件,而是返回null HttpSession httpSession = request.getSession(false); // 從當前請求的session物件 httpSession 中獲取安全上下文物件 SecurityContext context = readSecurityContextFromSession(httpSession); if (context == null) { // 邏輯走到這裡說明可能出現了以下情況: // 1. 當前請求對應的session物件不存在 // 2. session物件中的安全上下文物件為null // 3. session物件中的安全上下文物件不是類SecurityContext的例項 if (logger.isDebugEnabled()) { logger.debug("No SecurityContext was available from the HttpSession: " + httpSession + ". " + "A new one will be created."); } // 建立一個新的空的安全上下文SecurityContext物件 context = generateNewContext(); } SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper( response, request, httpSession != null, context); requestResponseHolder.setResponse(wrappedResponse); if (isServlet3) { requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper( request, wrappedResponse)); } return context; } // 儲存安全上下文到session屬性 // 會被 SecurityContextPersistenceFilter 在一個請求處理結束返回響應結果時呼叫 public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils .getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class); if (responseWrapper == null) { throw new IllegalStateException( "Cannot invoke saveContext on response " + response + ". You must use the HttpRequestResponseHolder.response after invoking loadContext"); } // saveContext() might already be called by the response wrapper // if something in the chain called sendError() or sendRedirect(). This ensures we // only call it // once per request. if (!responseWrapper.isContextSaved()) { responseWrapper.saveContext(context); } } // 檢查請求的session中是否已經含有非null安全上下文物件 public boolean containsContext(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return false; } return session.getAttribute(springSecurityContextKey) != null; } /** * 從session物件中獲取安全上下文物件 * @param httpSession the session obtained from the request. */ private SecurityContext readSecurityContextFromSession(HttpSession httpSession) { final boolean debug = logger.isDebugEnabled(); if (httpSession == null) { if (debug) { logger.debug("No HttpSession currently exists"); } return null; } // Session exists, so try to obtain a context from it. Object contextFromSession = httpSession.getAttribute(springSecurityContextKey); if (contextFromSession == null) { if (debug) { logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT"); } return null; } // We now have the security context object from the session. if (!(contextFromSession instanceof SecurityContext)) { if (logger.isWarnEnabled()) { logger.warn(springSecurityContextKey + " did not contain a SecurityContext but contained: '" + contextFromSession + "'; are you improperly modifying the HttpSession directly " + "(you should always use SecurityContextHolder) or using the HttpSession attribute " + "reserved for this class?"); } return null; } if (debug) { logger.debug("Obtained a valid SecurityContext from " + springSecurityContextKey + ": '" + contextFromSession + "'"); } // Everything OK. The only non-null return from this method. return (SecurityContext) contextFromSession; } /** * By default, calls SecurityContextHolder#createEmptyContext() to obtain a * new context (there should be no context present in the holder when this method is * called). Using this approach the context creation strategy is decided by the * SecurityContextHolderStrategy in use. The default implementations will * return a new SecurityContextImpl. * 預設情況下,呼叫SecurityContextHolder#createEmptyContext()獲取一個新的空的安全上下文物件 * (此時安全上下文持有器SecurityContextHolder中應該不能有安全上下文物件存在)。實際上是否使用 * 哪種安全上下文物件建立策略由SecurityContextHolder所使用的SecurityContextHolderStrategy * 決定。預設的實現是返回一個新建的SecurityContextImpl物件。 * @return a new SecurityContext instance. Never null. */ protected SecurityContext generateNewContext() { return SecurityContextHolder.createEmptyContext(); } /** * If set to true (the default), a session will be created (if required) to store the * security context if it is determined that its contents are different from the * default empty context value. * * Note that setting this flag to false does not prevent this class from storing the * security context. If your application (or another filter) creates a session, then * the security context will still be stored for an authenticated user. * * @param allowSessionCreation */ public void setAllowSessionCreation(boolean allowSessionCreation) { this.allowSessionCreation = allowSessionCreation; } /** * Allows the use of session identifiers in URLs to be disabled. Off by default. * * @param disableUrlRewriting set to true to disable URL encoding methods in * the response wrapper and prevent the use of jsessionid parameters. */ public void setDisableUrlRewriting(boolean disableUrlRewriting) { this.disableUrlRewriting = disableUrlRewriting; } /** * Allows the session attribute name to be customized for this repository instance. * 在session儲存安全上下文物件時所使用的屬性名稱可以通過這裡定製,預設是: * SPRING_SECURITY_CONTEXT * @param springSecurityContextKey the key under which the security context will be * stored. Defaults to #SPRING_SECURITY_CONTEXT_KEY. */ public void setSpringSecurityContextKey(String springSecurityContextKey) { Assert.hasText(springSecurityContextKey, "springSecurityContextKey cannot be empty"); this.springSecurityContextKey = springSecurityContextKey; } // ~ Inner Classes // ================================================================================================== private static class Servlet3SaveToSessionRequestWrapper extends HttpServletRequestWrapper { private final SaveContextOnUpdateOrErrorResponseWrapper response; public Servlet3SaveToSessionRequestWrapper(HttpServletRequest request, SaveContextOnUpdateOrErrorResponseWrapper response) { super(request); this.response = response; } @Override public AsyncContext startAsync() { response.disableSaveOnResponseCommitted(); return super.startAsync(); } @Override public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { response.disableSaveOnResponseCommitted(); return super.startAsync(servletRequest, servletResponse); } } /** * Wrapper that is applied to every request/response to update the * HttpSession with the SecurityContext when a sendError() or * sendRedirect happens. See SEC-398. * response包裝器,包裝之後,每個request/response在sendError(),或者sendRedirect發生時 * 會更新session中的安全上下文物件,參考 SEC-398。 * * Stores the necessary state from the start of the request in order to make a * decision about whether the security context has changed before saving it. */ final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper { private final HttpServletRequest request; private final boolean httpSessionExistedAtStartOfRequest; private final SecurityContext contextBeforeExecution; private final Authentication authBeforeExecution; /** * Takes the parameters required to call saveContext() successfully * in addition to the request and the response object we are wrapping. * * @param request the request object (used to obtain the session, if one exists). * @param httpSessionExistedAtStartOfRequest indicates whether there was a session * in place before the filter chain executed. If this is true, and the session is * found to be null, this indicates that it was invalidated during the request and * a new session will now be created. * @param context the context before the filter chain executed. The context will * only be stored if it or its contents changed during the request. */ SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request, boolean httpSessionExistedAtStartOfRequest, SecurityContext context) { super(response, disableUrlRewriting); this.request = request; this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest; this.contextBeforeExecution = context; this.authBeforeExecution = context.getAuthentication(); } /** * Stores the supplied security context in the session (if available) and if it * has changed since it was set at the start of the request. If the * AuthenticationTrustResolver identifies the current user as anonymous, then the * context will not be stored. * * @param context the context object obtained from the SecurityContextHolder after * the request has been processed by the filter chain. * SecurityContextHolder.getContext() cannot be used to obtain the context as it * has already been cleared by the time this method is called. * */ @Override protected void saveContext(SecurityContext context) { final Authentication authentication = context.getAuthentication(); HttpSession httpSession = request.getSession(false); // See SEC-776 if (authentication == null || trustResolver.isAnonymous(authentication)) { if (logger.isDebugEnabled()) { logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession."); } if (httpSession != null && authBeforeExecution != null) { // SEC-1587 A non-anonymous context may still be in the session // SEC-1735 remove if the contextBeforeExecution was not anonymous httpSession.removeAttribute(springSecurityContextKey); } return; } if (httpSession == null) { httpSession = createNewSessionIfAllowed(context); } // If HttpSession exists, store current SecurityContext but only if it has // actually changed in this thread (see SEC-37, SEC-1307, SEC-1528) if (httpSession != null) { // We may have a new session, so check also whether the context attribute // is set SEC-1561 if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) { httpSession.setAttribute(springSecurityContextKey, context); if (logger.isDebugEnabled()) { logger.debug("SecurityContext '" + context + "' stored to HttpSession: '" + httpSession); } } } } private boolean contextChanged(SecurityContext context) { return context != contextBeforeExecution || context.getAuthentication() != authBeforeExecution; } private HttpSession createNewSessionIfAllowed(SecurityContext context) { if (isTransientAuthentication(context.getAuthentication())) { return null; } if (httpSessionExistedAtStartOfRequest) { if (logger.isDebugEnabled()) { logger.debug("HttpSession is now null, but was not null at start of request; " + "session was invalidated, so do not create a new session"); } return null; } if (!allowSessionCreation) { if (logger.isDebugEnabled()) { logger.debug("The HttpSession is currently null, and the " + HttpSessionSecurityContextRepository.class.getSimpleName() + " is prohibited from creating an HttpSession " + "(because the allowSessionCreation property is false) - SecurityContext thus not " + "stored for next request"); } return null; } // Generate a HttpSession only if we need to if (contextObject.equals(context)) { if (logger.isDebugEnabled()) { logger.debug("HttpSession is null, but SecurityContext has not changed from default empty context: ' " + context + "'; not creating HttpSession or storing SecurityContext"); } return null; } if (logger.isDebugEnabled()) { logger.debug("HttpSession being created as SecurityContext is non-default"); } try { return request.getSession(true); } catch (IllegalStateException e) { // Response must already be committed, therefore can't create a new // session logger.warn("Failed to create a session, as response has been committed. Unable to store" + " SecurityContext."); } return null; } } private boolean isTransientAuthentication(Authentication authentication) { return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null; } /** * Sets the AuthenticationTrustResolver to be used. The default is * AuthenticationTrustResolverImpl. * * @param trustResolver the AuthenticationTrustResolver to use. Cannot be * null. */ public void setTrustResolver(AuthenticationTrustResolver trustResolver) { Assert.notNull(trustResolver, "trustResolver cannot be null"); this.trustResolver = trustResolver; } }