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;
}
}