登录成功之后,在后续的业务逻辑中,开发者可能还需要获取登录成功的用户对象,如果不使用任何安全管理框架,那么可以将用户信息保存在HttpSession中,以后需要的时候直接从HttpSession中获取数据。在Spring Security中,用户登录信息本质上还是保存在 HttpSession中,但是为了方便使用,Spring Security对HttpSession中的用户信息进行了封装, 封装之后,开发者若再想获取用户登录数据就会有两种不同的思路:
这里列出来的两种方式是主流的做法,开发者也可以使用一些非主流的方式获取登录成功后的用户信息,例如直接从HttpSession中获取用户登录数据,
无论是哪种获取方式,都离不开一个重要的对象:Authentication。在Spring Security中, Authentication对象主要有两方面的功能:
一个Authentication对象主要包含三个方面的信息:
Java中本身提供了 Principal接口用来描述认证主体,Principal可以代表一个公司、个人或者登录ID,Spring Security中定义了 Authentication接口用来规范登录用户信息, Authentication 继承自 Principal:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
这里接口中定义的方法都很好理解:
可以看到,在Spring Security中,只要获取到Authentication对象,就可以获取到登录用户的详细信息,
不同的认证方式对应不同的Authentication实例,Spring Security中的Authentication实现类如图2-11所示,
这些实现类现看起来可能会觉得陌生,不过没关系,在后续中,这些实现类基本上都会涉及,现在我们先对每个类的功能做一个大概介绍:
在这些 Authentication 的实例中,最常用的有两个:UsernamePasswordAuthenticationToken 和RememberMeAuthenticationToken。在2中的案例对应的用户认证对象就是Username PasswordAuthenticationToken。
了解了 Authentication对象之后,接下来我们来看一下如何在登录成功后获取用户登录信 息,即 Authentication 对象。
图 2-11
添加一个UserController,内容如下:
package com.intehel.demo.controller; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Collection; @RestController public class UserController { @GetMapping("/user") public void userinfo(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String name = authentication.getName(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); System.out.println("name = "+name); System.out.println("authorities = "+authorities); } }
配置完成后,启动项目,登录成功后,访问/user接口,控制台就会打印出登录用户信息, 当然,由于我们目前没有给用户配置角色,所以默认的用户角色为空数组,如图2-12所示。
图 2-12
这里为了演示方便,我们在Controller中获取登录用户信息,可以发现,SecurityContextHolder.getContext()是一个静态方法,也就意味着我们随时随地都可以获取到登录用户信息,在service层也可以获取到登录用户信息(在实际项目中,大部分情况下也都是在service层获取登录用户信息)。
获取登录用户信息的代码很简单,那么SecuntyContextHolder到底是什么?它里边的数据又是从何而来的?接下来我们将进行一一解析。
SecurityContextHolder 中存储的是 SecurityContext,SecurityContext 中存储的则是 Authentication,三者的关系如图2-13所示。
图 2-13
这幅图清晰地描述了 SecurityContextHolder、SecurityContext 以及 Authentication 三者之间 的关系。
首先在 SecurityContextHolder 中存放的是 SecurityContext, SecurityContextHolder 中定义了三种不同的数据存储策略,这实际上是一种典型的策略模式:
Spring Security中定义了 SecurityContextHolderStrategy接口用来规范存储策略中的方法, 我们来看一下:
public interface SecurityContextHolderStrategy { void clearContext(); SecurityContext getContext(); void setContext(SecurityContext context); SecurityContext createEmptyContext(); }
接口中一共定义了四个方法:
在 Spring Security 中,SecurityContextHolderStrategy接口一共有三个实现类,对应了三种不同的存储策略,如图2-14所示
图 2-14
每一个实现类都对应了不同的实现策略,我们先来看一下ThreadLocalSecurityContextHolderStrategy:
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>(); public void clearContext() { contextHolder.remove(); } public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); if (ctx == null) { ctx = createEmptyContext(); contextHolder.set(ctx); } return ctx; } public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); contextHolder.set(context); } public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } }
ThreadLocalSecurityContextHolderStrategy实现了 SecurityContextHolderStrategy接口,并实现了接口中的方法,存储数据的载体就是一个ThreadLocal,所以针对SecurityContext的清空、获取以及存储,都是在ThreadLocal中进行操作,例如清空就是调用ThreadLocal的remove 方法。SecurityContext是一个接口,它只有一个实现类SecurityContextImpl,所以创建就直接新建一个SecurityContextImpl对象即可。
再来看 InheritableThreadLocalSecurityContextHolderStrategy:
final class InheritableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal<>(); public void clearContext() { contextHolder.remove(); } public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); if (ctx == null) { ctx = createEmptyContext(); contextHolder.set(ctx); } return ctx; } public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); contextHolder.set(context); } public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } }
InheritableThreadLocalSecurityContextHolderStrategy 和 ThreadLocalSecurityContextHolderStrategy的实现策略基本一致,不同的是存储数据的载体变了,在InheritableThreadLocalSecurityContextHolderStrategy 中存储数据的载体变成了 InheritableThreadLocal。InheritableThreadLocal继承自ThreadLocaL但是多了一个特性,就是在子线程创建的一瞬间,会自动将父线程中的数据复制到子线程中。该存储策略正是利用了这一特性,实现了在子线程中获取登录用户信息的功能,
最后再来看一下 GlobalSecurityContextHolderStrategy:
final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static SecurityContext contextHolder; public void clearContext() { contextHolder = null; } public SecurityContext getContext() { if (contextHolder == null) { contextHolder = new SecurityContextImpl(); } return contextHolder; } public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); contextHolder = context; } public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } }
GlobalSecurityContextHolderStrategy的实现就更简单了,用一个静态变量来保存 SecurityContext,所以它也可以在多线程环境下使用,但是一般在Web开发中,这种存储策略使用得较少。
public class SecurityContextHolder { public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; public static final String SYSTEM_PROPERTY = "spring.security.strategy"; private static String strategyName = System.getProperty(SYSTEM_PROPERTY); private static SecurityContextHolderStrategy strategy; private static int initializeCount = 0; static { initialize(); } public static void clearContext() { strategy.clearContext(); } public static SecurityContext getContext() { return strategy.getContext(); } public static int getInitializeCount() { return initializeCount; } private static void initialize() { if (!StringUtils.hasText(strategyName)) { strategyName = MODE_THREADLOCAL; } if (strategyName.equals(MODE_THREADLOCAL)) { strategy = new ThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals(MODE_GLOBAL)) { strategy = new GlobalSecurityContextHolderStrategy(); } else { try { Class<?> clazz = Class.forName(strategyName); Constructor<?> customStrategy = clazz.getConstructor(); strategy = (SecurityContextHolderStrategy) customStrategy.newInstance(); } catch (Exception ex) { ReflectionUtils.handleReflectionException(ex); } } initializeCount++; } public static void setContext(SecurityContext context) { strategy.setContext(context); } public static void setStrategyName(String strategyName) { SecurityContextHolder.strategyName = strategyName; initialize(); } public static SecurityContextHolderStrategy getContextHolderStrategy() { return strategy; } public static SecurityContext createEmptyContext() { return strategy.createEmptyContext(); } @Override public String toString() { return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]"; } }
这段源码中可以看到,SecurityContextHolder定义了三个静态常量用来描述三种不同的存储策略;存储策略strategy会在静态代码块中进行初始化,根据不同的strategyName初始化不同的存储策略;strategyName变量表示目前正在使用的存储策略,开发者可以通过配置系统变量或者调用setStrategyName来修改SecurityContextHolder中的存储策略,调用 setStrategyName 后会重新初始化 strategy。
默认情况下,如果开发者试图从子线程中获取当前登录用户数据,就会获取失败,代码如下:
package com.intehel.demo.controller; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Collection; @RestController public class UserController { @GetMapping("/user") public void userinfo(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String name = authentication.getName(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); System.out.println("name = "+name); System.out.println("authorities = "+authorities); new Thread(new Runnable() { @Override public void run() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { System.out.println("获取用户信息失败"); } String name = authentication.getName(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); String threadName = Thread.currentThread().getName(); System.out.println(threadName+":name = "+name); System.out.println(threadName+":authorities = " + authorities); } }).start(); } }
在子线程中尝试获取登录用户数据时,获取到的数据为null,如图2-15所示。
图 2-15
子线程之所以获取不到登录用户信息,就是因为数据存储在ThreadLocal中,存储和读取不是同一个线程,所以获取不到。如果希望子线程中也能够获取到登录用户信息,可以将 SecurityContextHolder 中的存储策略改为 MODE INHERITABLETHREADLOCAL,这样就支持多线程环境下获取登录用户信息了。
默认的存储策略是通过System.getPropeity加载的,因此我们可以通过配置系统变量来修改默认的存储策略,以IntelliJ IDEA为例,首先单击启动按钮,选择Edit Configurations按钮, 如图2-16所示,然后在打开的选项中,配置VM options参数,添加如下一行,配置界而如图 2-17所示
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
图 2-16
图 2-17
这样,在SecurityContextHolder中通过System.getProperty加载到的默认存储策略就支持多线程环境了,
配置完成之后,再次启动项目,此时访问/user接口,即使在子线程中,也可以获取到登录用户信息了,如图2-18所示。
图 2-18
那么既然SecurityContextHolder默认是将用户信息存储在 ThreadLocal中,在Spring Boot中不同的请求都是由不同的线程处理的,那为什么每一次请求都还能从SecurityContextHolder中获取到登录用户信息呢?这就不得不提到Spring Security过滤器链中重要—环 SecurityContextPersistenceFliter。
前面介绍了 Spring Security中的常见过滤器,在这些过滤器中,存在一个非常重要的过滤器就是 SecurityContextPersistenceFliter。
默认情况下,在 Spring Security 过滤器链中,SecurityContextPersistenceFilter 是第一道防线,位于 WebAsyncManagerlntegrationFilter 之后。从 SecurityContextPersistenceFilter 这个过滤器的名字上就可以推断出来,它的作用是为了存储SecuntyContext而设计的。
整体上来说,SecurityContextPersistenceFilter 主要做两件事情:
在SecurityContextPersistenceFilter 过滤器中,当一个请求处理完毕时,从SecurityContextHolder中获取SecurityContext 存入 HttpSession 中,这一步的操作主要是针对异步Servlet。如果不是异步Servlet,在响应提交时,就会将
SecurityContext 保存到HttpSession 中了,而不会等到在SecurityContextPersistenceFliter过滤器中再去存储。
这就是SecurityContextPersistenceFliter大致上做的事情,在正式升始介绍SecurityContextPersistenceFilter 之前,需要先介绍另外接口,这就是 SecurityContextRepository接口,将 SecurityContext 存入 HttpSession,或者从 HttpSession 中加载数据并转为 SecurityContext对象,这些事情都是由SecurityContextRepositoiy接口的实现类完成的,因此这里我们 就先从 SecurityContextRepositoiy 接口 升始看起。首先我们来看一下SecurityContextRepository接口的定义:
public interface SecurityContextRepository { SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder); void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response); boolean containsContext(HttpServletRequest request); }
SecurityContextRepository接口 中一共定义了三个方法:
在Spring Security框架中,为SecurityContextRepository接口一共提供了三个实现类,如图2-19所示。
图 2-19
在这三个实现类中,TestSecurityContextRepository为单元测试提供支持;NullSecurityContextRepository 实现类中,loadContext 方法总是返回一个空的 SecurityContext 对象, saveContext方法未做任何实现,containsContext方法总是返回false ,所以 NullSecurityContextRepository 实现类实际上未做 SecurityContext 的存储工作,
在 Spring Security 中默认使用的实现类是 HttpSessionSecurityContextRepository,通过 HttpSessionSecurityContextRepository实现了将 SecurityContext 存储到 HttpSession 以及从 HttpSession 中加载 SecurityContext出来,这里我们来重点看一下 HttpSessionSecurityContextRepository 类。
在正式开始介绍 HttpSessionSecurityContextRepository之前,首先来看一下 HttpSessionSecurityContextRepository中定义的关于请求和封装的两个内部类。
首先是 HttpSessionSecurityContextRepository中定义的对于响应的封装类 SaveToSessionResponseWrapper,我们先来看一下 SaveToSessionResponseWrapper的继承关系图,如图 2-20 所示。
图 2-20
从这幅继承关系图中可以看到,SaveToSessionResponseWrapper实际上就是我们所熟知的 HttpServletResponse功能的扩展。这里有三个关键的实现类:
接下来看一下 HttpSessionSecurityContextRepositoiy 中 SaveToSessionResponseWrapper 的定义:
final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper { private final HttpServletRequest request; private final boolean httpSessionExistedAtStartOfRequest; private final SecurityContext contextBeforeExecution; private final Authentication authBeforeExecution; 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(); } @Override protected void saveContext(SecurityContext context) { final Authentication authentication = context.getAuthentication(); HttpSession httpSession = request.getSession(false); 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) { httpSession.removeAttribute(springSecurityContextKey); } return; } if (httpSession == null) { httpSession = createNewSessionIfAllowed(context); } if (httpSession != null) { 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 (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; } 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) { logger.warn("Failed to create a session, as response has been committed. Unable to store" + " SecurityContext."); } return null; } }
在 SaveToSessionResponseWrapper 中其实主要定义了三个方法:saveContext、contextChanged以及CreateNewSessionIfAllowed
这就是 HttpSessionSecurityContextRepositoiy 中封装的 SaveToSessionResponseWrapper 对象,一个核心功能就是在HttpServletResponse提交的时候,将SecurityContext保存到HttpSession 中,
接下来看—下 HttpSessionSecurityContextRepositoiy 中关于 SaveToSessionResponseWrapper 的定义,SaveToSessionResponseWrapper相对而言就要简单很多了:
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); } }
SaveToSessionRequestWrapper类实际上是在Spring Security 3.2之后出现的封装类,在 Spring Security 3.2 之前并不存在 SaveToSessionRequestWrapper 类。封装的 SaveToSessionRequestwrapper类主要作用是禁止在异步Servlet提交时,自动保存SecurityContext
为什么要禁止呢?我们来看如下一段简单的代码:
@GetMapping("/user2") public void userinfo(HttpServletRequest req, HttpServletResponse resp){ AsyncContext asyncContext = req.startAsync(); CompletableFuture.runAsync(()->{ try { PrintWriter out = asyncContext.getResponse().getWriter(); out.write("hello buretuzi"); asyncContext.complete(); }catch (IOException e){ e.printStackTrace(); } }); }
可以看到,在异步Servlet中,当任务执行完毕之后,HttpServletResponse也会自动提交, 在提交的过程中会自动保存SecurityContext到HttpSession中,但是由于是在子线程中,因此无法获取到 SecurityContext 对象(SecurityContextHolder 默认将数据存储在 ThreadLocal 中), 所以会保存失败。如果开发者使用了异步Servlet,则默认情况下会禁用HttpServletResponse 提交时自动保存SecurityContext这一功能,改为在SecurityContextPersistenceFilter过滤器中完成 SecurityContext 保存操作。
看完了 HttpSessionSecurityContextRepositoiy中封装的两个请求/响应对象之后,接下来我 们再来整体上看一下 HttpSessionSecurityContextRepositoiy 类的功能
public class HttpSessionSecurityContextRepository implements SecurityContextRepository { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; protected final Log logger = LogFactory.getLog(this.getClass()); 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(); public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { HttpServletRequest request = requestResponseHolder.getRequest(); HttpServletResponse response = requestResponseHolder.getResponse(); HttpSession httpSession = request.getSession(false); SecurityContext context = readSecurityContextFromSession(httpSession); if (context == null) { if (logger.isDebugEnabled()) { logger.debug("No SecurityContext was available from the HttpSession: " + httpSession + ". " + "A new one will be created."); } context = generateNewContext(); } SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper( response, request, httpSession != null, context); requestResponseHolder.setResponse(wrappedResponse); if (isServlet3) { requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper( request, wrappedResponse)); } return context; } 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"); } if (!responseWrapper.isContextSaved()) { responseWrapper.saveContext(context); } } public boolean containsContext(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return false; } return session.getAttribute(springSecurityContextKey) != null; } private SecurityContext readSecurityContextFromSession(HttpSession httpSession) { final boolean debug = logger.isDebugEnabled(); if (httpSession == null) { if (debug) { logger.debug("No HttpSession currently exists"); } return null; } Object contextFromSession = httpSession.getAttribute(springSecurityContextKey); if (contextFromSession == null) { if (debug) { logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT"); } return null; } 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 + "'"); } return (SecurityContext) contextFromSession; } protected SecurityContext generateNewContext() { return SecurityContextHolder.createEmptyContext(); } public void setAllowSessionCreation(boolean allowSessionCreation) { this.allowSessionCreation = allowSessionCreation; } public void setDisableUrlRewriting(boolean disableUrlRewriting) { this.disableUrlRewriting = disableUrlRewriting; } public void setSpringSecurityContextKey(String springSecurityContextKey) { Assert.hasText(springSecurityContextKey, "springSecurityContextKey cannot be empty"); this.springSecurityContextKey = springSecurityContextKey; } 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); } } final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper { private final HttpServletRequest request; private final boolean httpSessionExistedAtStartOfRequest; private final SecurityContext contextBeforeExecution; private final Authentication authBeforeExecution; 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(); } @Override protected void saveContext(SecurityContext context) { final Authentication authentication = context.getAuthentication(); HttpSession httpSession = request.getSession(false); 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) { httpSession.removeAttribute(springSecurityContextKey); } return; } if (httpSession == null) { httpSession = createNewSessionIfAllowed(context); } if (httpSession != null) { 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 (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; } 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) { logger.warn("Failed to create a session, as response has been committed. Unable to store" + " SecurityContext."); } return null; } } public void setTrustResolver(AuthenticationTrustResolver trustResolver) { Assert.notNull(trustResolver, "trustResolver cannot be null"); this.trustResolver = trustResolver; } }
这就是HttpSessionSecurityContextRepositoiy所提供的所有功能,这些功能都将在 SecurityContextPersistenceFilter过滤器中进行调用,那么接下来我们就来看一下SecurityContextPersistenceFilter中的调用逻辑:
public class SecurityContextPersistenceFilter extends GenericFilterBean { static final String FILTER_APPLIED = "__spring_security_scpf_applied"; private SecurityContextRepository repo; private boolean forceEagerSessionCreation = false; public SecurityContextPersistenceFilter() { this(new HttpSessionSecurityContextRepository()); } public SecurityContextPersistenceFilter(SecurityContextRepository repo) { this.repo = repo; } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null) { chain.doFilter(request, response); return; } final boolean debug = logger.isDebugEnabled(); request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (forceEagerSessionCreation) { HttpSession session = request.getSession(); if (debug && session.isNew()) { logger.debug("Eagerly created session: " + session.getId()); } } HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } } public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) { this.forceEagerSessionCreation = forceEagerSessionCreation; } }
过滤器的核心方法当然是doFilter,我们就从doFilter方法开始介绍:
这就是整个SecurityContextPersistenceFilter过滤器的工作逻辑。一言以蔽之,请求在到达 SecurityContextPersistenceFilter 过滤器之后,先从 HttpSession 中读取 SecurityContext 出来,并存入SecurityContextHolder之中以备后续使用;当请求离开 SecurityContextPersisteiiceFilter过滤器的时候,获取最新的SecurityContext并存入HttpSession中,同时清空SecurityContextHolder中的登录用户信息。
这就是第一种登录数据的获取方式,即从SecurityContextHolder中获取。
接下来我们来看一下第二种登录数据获取方式——从当前请求中获取。获取代码如下:
@RequestMapping("/authentication") public void authentication(Authentication authentication){ System.out.println("Authentication = " + authentication); } @RequestMapping("/principal") public void principal(Principal principal){ System.out.println("Principal = " + principal); }
开发者可以直接在Controller的请求参数中放入Authentication对象来获取登录用户信息。 通过前面的讲解,大家已经知道Authentication是Principal的子类,所以也可以直接在请求参数中放入Principal来接收当前登录用户信息,需要注意的是,即使参数是PrincipaL,真正的实例依然是Authentication的实例。
用过Spring MVC的读者都知道,Controller中方法的参数都是当前请求HttpServletRequest 带来的。毫无疑问,前面的Authentication和Principal参数也都是HttpServletRequest带来的, 那么这些数据到底是何时放入HttpServletRequest的呢?又是以何种形式存在的呢?接下来我 们一起分析一下,
在Servlet规范中,最早有三个和安全管理相关的方法:
public String getRemoteUser();
public boolean isUserlnRole(String role);
public java.security.Principal getUserPrincipal();
从Servlet 3.0开始,在这三个方法的基础之上,又增加了三个和安全管理相关的方法:
public boolean authenticate(HttpServletRespouse response)
throws lOException, ServletException;
public void login(String username, String password) throws ServletException;
public void logout() throws ServletException
不过HttpServletRequest只是一个接口,这些安全认证相关的方法,在不同环境下会有不同的实现。
如果是一个普通的Web项目 ,不使用任何框架,HttpServletRequest的默认实现类是Tomcat 中的RequestFacade,从这个类的名字上就可以看出来,这是一个使用了 Facade模式(外观模式)的类,真正提供底层服务的是Tomcat中的Request对象,只不过这个Request对象在实现 Servlet规范的同时,还定义了很多Tomcat内部的方法,为了避免开发者直接调用到这些内部方法,这里使用了外观模式。
在Tomcat的Request类中,对上面这些方法都做了实现,基本上都是基于Tomcat提供的 Realm来实现的,这种认证方式非常冷门,项目中很少使用,因此这里不做过多介绍,感兴趣 的读者可以查看 https://github.com/lenve/javaboy-code-samples 仓库中的 basiclogin 案例来了解 其用法。
如果使用了 Spring Security框架,那么我们在Contioller参数中拿到的HttpServletRequest 实例将是 Servlet3SecurityContextHolderAwareRequestWrapper,很明显,这是被 Spring Security 封装过的请求,
我们来看一下 Servlet3SecurityContextHolderAwareRequestWrapper 的继承关系,如图 2-21 所示。
图 2-21
HttpServletRequestWrapper 就不用过多介绍了 , SecurityContextHolderAwareRequestWrapper类主要实现了 Servlet 3.0之前和安全管理相关的三个方法,也就是getRemoteUser()、isUserlnRole(String)以及getUserPrincipalQ,Servlet 3.0中新增的三个安全管理相关的方法, 则在Servlet3SecurityContextHolderAwareRequestWrapper类中实现心获取用户登录信息主要和前面三个方法有关,因此这里我们主要来看一下SecurityContextHolderAwareRequestWrapper 类中相关方法的实现。
public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper { private final AuthenticationTrustResolver trustResolver; private final String rolePrefix; public SecurityContextHolderAwareRequestWrapper(HttpServletRequest request, String rolePrefix) { this(request, new AuthenticationTrustResolverImpl(), rolePrefix); } public SecurityContextHolderAwareRequestWrapper(HttpServletRequest request, AuthenticationTrustResolver trustResolver, String rolePrefix) { super(request); Assert.notNull(trustResolver, "trustResolver cannot be null"); this.rolePrefix = rolePrefix; this.trustResolver = trustResolver; } private Authentication getAuthentication() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (!trustResolver.isAnonymous(auth)) { return auth; } return null; } @Override public String getRemoteUser() { Authentication auth = getAuthentication(); if ((auth == null) || (auth.getPrincipal() == null)) { return null; } if (auth.getPrincipal() instanceof UserDetails) { return ((UserDetails) auth.getPrincipal()).getUsername(); } return auth.getPrincipal().toString(); } @Override public Principal getUserPrincipal() { Authentication auth = getAuthentication(); if ((auth == null) || (auth.getPrincipal() == null)) { return null; } return auth; } private boolean isGranted(String role) { Authentication auth = getAuthentication(); if (rolePrefix != null && role != null && !role.startsWith(rolePrefix)) { role = rolePrefix + role; } if ((auth == null) || (auth.getPrincipal() == null)) { return false; } Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); if (authorities == null) { return false; } for (GrantedAuthority grantedAuthority : authorities) { if (role.equals(grantedAuthority.getAuthority())) { return true; } } return false; } @Override public boolean isUserInRole(String role) { return isGranted(role); } @Override public String toString() { return "SecurityContextHolderAwareRequestWrapper[ " + getRequest() + "]"; } }
SecurityContextHolderAwareRequestWrapper 类其实非常好理解:
前面我们直接将Authentication或者Principal写到Controller参数中,实际上就是Spring MVC 框架从 Seivlet3SecurityContextHolderAwareRequestWrapper 中提取的用户信息。
那么Spring Security是如何将默认的请求对象转化为Servlet3SecurityContextHolderAwareRequestwrapper的呢?这就涉及Spring Security过滤器链中另外一个重要的过滤器---------------------------------------------SecurityContextHolderAwareRequestFilter。
前面我们提到 Spring Security 过滤器中,有一个 SecurityContextHolderAwareRequestFilter 过滤器,该过滤器的主要作用就是对HttpServletRequest请求进行再包装,重写 HttpServletRequest中和安全管理相关的方法心HttpSeivletRequest在整个请求过程中会被包装 多次,每一次的包装都会给它增添新的功能,例如在经过SecurityContextPersistenceFilter请求 时就会对它进行包装。
我们来看一下 SecurityContextHolderAwareRequestFilter 过滤器的源码(部分):
public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean { private String rolePrefix = "ROLE_"; private HttpServletRequestFactory requestFactory; private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationManager authenticationManager; private List<LogoutHandler> logoutHandlers; private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); public void setRolePrefix(String rolePrefix) { Assert.notNull(rolePrefix, "Role prefix must not be null"); this.rolePrefix = rolePrefix; updateFactory(); } public void setAuthenticationEntryPoint( AuthenticationEntryPoint authenticationEntryPoint) { this.authenticationEntryPoint = authenticationEntryPoint; } public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) { this.logoutHandlers = logoutHandlers; } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res); } @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); updateFactory(); } private void updateFactory() { String rolePrefix = this.rolePrefix; this.requestFactory = isServlet3() ? createServlet3Factory(rolePrefix) : new HttpServlet25RequestFactory(this.trustResolver, rolePrefix); } public void setTrustResolver(AuthenticationTrustResolver trustResolver) { Assert.notNull(trustResolver, "trustResolver cannot be null"); this.trustResolver = trustResolver; updateFactory(); } private HttpServletRequestFactory createServlet3Factory(String rolePrefix) { HttpServlet3RequestFactory factory = new HttpServlet3RequestFactory(rolePrefix); factory.setTrustResolver(this.trustResolver); factory.setAuthenticationEntryPoint(this.authenticationEntryPoint); factory.setAuthenticationManager(this.authenticationManager); factory.setLogoutHandlers(this.logoutHandlers); return factory; } private boolean isServlet3() { return ClassUtils.hasMethod(ServletRequest.class, "startAsync"); } }
从这段源码中可以看到,在 SecurityCoutextHolderAwareRequestFilter#doFilter 方法中,会 调用 requestFactory.create 方法对请求重新进行包装,requestFactory 就是 HttpServletRequest Factoiy 类的实例,它的 create 方法里边就直接创建 了一个 Servlet3SecurityContextHolderAwareRequestwrapper 实例。
对请求的HttpServletRequest包装之后,接下来在过滤器链中传递的HttpServletRequest对 象,它的 getRemoteUser()、isUserlnRole(String)以及 getUserPrincipal()方法就可以直接使用 了,
HttpServletRequest 中 getUserPrincipal()方法有了返回值之后,最终在 Spring MVC 的 ServletRequestMethodArgumentResolvei#resolveArgument(Class<?>, HttpServletRequest)方法中进行默认参数解析,自动解析出Principal对象,开发者在Controller中既可以通过Principal 来接收参数,也可以通过Authentication对象来接收。
经过前面的介绍,相信对于Spring Security中两种获取登录用户信息的方式,以及这两种获取方式的原理,都有一定的了解了,