欢迎你们前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~javascript
会话管理一直是 Java 企业级应用的重要部分。不过在很长的一段时间里,这一部分都被咱们认为是一个已解决的问题,而且也没有什么重大的创新出现。html5
然而,微服务还有可横向伸缩的云原生应用这一现代趋势揭露了现今的会话管理技术在设计上的一些缺陷,挑战着咱们在过去 20 多年来对这一设计得出的一些结论。java
本文会演示Spring Session API 为了帮助咱们克服之前的会话管理方式的一些局限所采起的方法。咱们将会先总结一下当前的会话管理技术的问题,而后深刻探讨 Spring Session 解决这些问题所采起的策略。最后,咱们会总结 Spring Session 的工做方式以及在具体项目里面的一些用法。git
Spring Session 为企业级 Java 应用的会话管理领域带来了革新,让咱们能够轻松作到:github
注意,Spring Session 项目其实并不依赖于 Spring 框架,所以咱们甚至能在不使用 Spring 框架的项目里面用到它。web
Spring Session 的目的是解决传统的 JavaEE 会话管理技术的各类问题。下面就经过一些例子说明一些这方面的问题。redis
从云原生应用架构的视角来看,一个应用应该能够经过在一个大型的虚拟机池里运行更多的 Linux 容器来部署更多的实例的方式来获得横向的伸缩。好比,咱们能很轻松地将一个这样的应用的 war 文件部署到 Cloud Foundry 或 Heroku 上的 Tomcat 里面,而后在几秒内扩展出 100 个应用程序实例,使得其中每一个实例都有 1GB 的 RAM。咱们还能够将云平台设置成会根据用户需求自动增减应用程序实例的数量。spring
不少应用都会把 HTTP 会话状态存储在运行应用代码的 JVM 里面。这很容易实现,并且存取的速度也很快。当一个应用实例加入或退出集群的时候,HTTP 会话的存储会在全部尚存的应用程序实例上从新进行平均的分配。在弹性云环境中,咱们会运行数以百计的应用实例,且实例数量可能随时发生快速的增减变化。这就带来了一些问题:apache
所以,将 HTTP 会话状态存储在运行应用代码的 JVM 以外的数据存储中会更高效。例如能够设置并使用 Redis 来存储上述的 100 个 Tomcat 实例里面的会话状态,那么 Tomcat 实例数量的增减便不会影响到在 Redis 中的会话存储的模式。另外,由于 Redis 是用 C 语言编写的,因此它能够在没有垃圾回收机制影响其运行的前提下,动用数百 GB 甚至 TB 数量级的内存。
对像 Tomcat 这样的开源服务器,找到使用外部数据存储(如 Redis 或 Memcached)的会话管理技术的其余实现是很简单的,可是使用起来的配置过程可能很复杂,而且每一个应用服务器的配置过程可能都不同。对如 WebSphere 和 Weblogic 之类的闭源产品,找到适合它们的会话管理技术的替代实现则一般是不可能的。
Spring Session 为设置插件式的会话数据存储提供了一种独立于具体应用服务器的方法,使得咱们能在 Servlet 框架的范畴内实现这样的存储,而不用依赖于具体的应用服务器的 API。这意味着 Spring Session 能够与全部实现了 Servlet 规范的应用服务器(Tomcat,Jetty,WebSphere,WebLogic,JBoss)协同工做,并在全部应用服务器上以彻底相同且很容易的方式来进行配置。
咱们还能够根据咱们的需求选用最适合的外部会话数据存储。这使得 Spring Session 也成了一个能帮助咱们将传统的 JavaEE 应用迁移到云端并做为一个符合十二要素的应用的一个理想的迁移工具。
假设你正在 example.com 上运行一个面向大众的 Web 应用,其中一些人类用户建立了多个账号。例如,用户 Jeff Lebowski 可能有两个账号 thedude@example.com 和 lebowski@example.com。跟其余 Java Web 应用程序同样,你可使用 HttpSession 来跟踪各类会话状态,好比当前登陆的用户。所以,当用户想从 dude@example.com 切换到 lebowski@example.com 时,就必须注销当前帐号并从新登陆。
使用 Spring Session 来为每一个用户配置多个 HTTP 会话就很简单了。这时 Jeff Lebowski 无需注销和登陆就能够在 thedude@example.com 和 lebowski@example.com 之间来回切换。
想象一下,你要构建一个具备复杂的自定义受权体系的 Web 应用,其中具备不一样权限的用户会具备不一样的应用 UI 样式。
好比说,假设应用有四个安全级别:公开(public)、保密(confidential)、机密(secret)以及绝密(top secret)。在用户登陆到应用时,系统会识别这一用户的安全级别,而后只对其显示不高于其安全级别的数据。这样,公开级别的用户能够看到公开级别的文档;具备保密级别的用户能看公开和保密级别的,以此类推。为了让用户界面更加友好,咱们的应用也应该能让用户预览应用的 UI 在较低的安全级别下的样子。好比绝密级别用户应该能在秘密模式下预览应用的各项事物的外观。
典型的 Web 应用会将当前用户的身份及其安全级别或角色存储在 HTTP 会话里面。不过,因为 Web 应用的每一个用户只有一个会话,所以也只能经过注销再登陆的方式来切换用户的角色,或者实现一个用户多个会话这一形式。
凭借 Spring Session,咱们就能够很轻松地给每一个登陆用户建立多个相互独立的会话,预览功能的实现也会所以变得简单。好比当前以绝密等级登陆的用户想要预览机密等级下的应用时,就能够对其建立并使用一个新的安全级别为机密的会话。
再想象一个场景,在用户经过 example.com 登陆到咱们的 Web 应用时,他们能使用经过 Websockets 工做的一个 HTML5 即时聊天客户端进行对话。不过,根据 Servlet 规范,经过 Websockets 发出的请求不会更新会话的过时时间,所以在用户进行聊天的时候,不管他们的聊天有多频繁,会话也可能聊着聊着就没了,而后 Websocket 链接也会所以关闭,聊天也就没法继续了。
又是凭借 Spring Session,咱们能够很轻松地确保 Websocket 请求还有常规的 HTTP 请求都能更新会话的过时时间。
再想象一下,咱们的应用提供了两种访问方式,一个基于 HTTP 的 RESTful API,另外一个是基于 RabbitMQ 的 AMQP 消息。此时,执行处理 AMQP 消息的的线程是没法访问应用服务器的 HttpSession
的,对此咱们必须本身写一个解决方案来访问 HTTP 会话里边的数据。
仍是凭借 Spring Session,只要咱们知道会话的 ID,就能够从应用程序的任意线程访问 Spring Session。Spring Session 比以往的 Servlet HTTP 会话管理器有着功能更加丰富的 API,使得咱们只须要知道会话 ID 就能定位咱们想要找的会话。好比,咱们能够用传入消息的用户标识字段来直接找到对应的会话。
如今传统应用服务器在 HTTP 会话管理方面的局限性已经在不一样情境中展现过了,咱们再来看看 Spring Session 是如何解决这些问题的。
在实现一个会话管理器的时候,有两个关键问题必须获得解决:
不过在本质上,有个更关键的问题是:如何跨越不一样的请求协议来传输一个会话的 ID?
第一个问题对 Spring Session 来讲已被各类高可用可伸缩的集群存储(Redis、Gemfire、Apache Geode 等)很好地解决了。所以 Spring Session 也应该定义一组标准接口来使得对底层数据存储的访问能够用不一样的数据存储来实现。Spring Session 在定义 Session
和 ExpiringSession
这些基本的关键接口以外,也针对了不一样数据存储的访问定义了关键接口 SessionRepository
。
org.springframework.session.Session
是定义会话基本功能的接口,例如属性的设置和删除。这个接口并不依赖于具体的底层技术,所以能够比 Servlet 里面的 HttpSession
适用于更多的状况;org.springframework.session.ExpiringSession
则扩展了 Session 接口。它提供了一些属性,让咱们能够设置具备时效性的会话,并查询这个会话是否已通过期。RedisSession
即是这个接口的一个实现范例。org.springframework.session.SessionRepository
定义了建立,保存,删除和查找会话的方法。将 Session 保存到数据存储的实际逻辑便写在这一接口的具体实现中。例如 RedisOperationsSessionRepository
即是这个接口的一个实现,它使用 Redis 来实现了会话的建立、保存以及删除。至于将请求关联到特定会话实例的问题,Spring Session 则假定这一关联的过程取决于特定的协议,由于客户端和服务器在请求 / 响应周期期间就须要对所传输的会话 ID 达成一致。好比,若是客户端发来一个 HTTP 请求,那么会话就能够经过 Cookie 或者 HTTP 报文首部来和请求相关联。若是发来一个 HTTPS 请求,则可用 SSL 的 Session ID 字段来说会话与请求相关联。若发来的是 JMS 消息,那也能够用消息首部来存储请求和响应间的会话 ID。
对 HTTP 协议的关联操做,Spring 会话定义了一个 HttpSessionStrategy
接口,后者有将 Cookies 和会话关联在一块儿的 CookieHttpSessionStrategy
和使用了自定义报文首部字段来管理会话的 HeaderHttpSessionStrategy
两种实现。
下面便详细地介绍一下 Spring Session 在 HTTP 协议上的工做方式。
在本文发布时(2015.11.10),Spring Session 1.0.2 在当前的 GA 发行版提供了使用 Redis 的 Spring Session 的一套实现,以及支持任何分布式的 Map(如 Hazelcast)的实现。其实,实现 Spring Session 针对某种数据存储的支持是相对容易的,在开源社区里已经有了不少这样的实现。
基于 HTTP 的 Spring Session 是以一个标准 Servlet 过滤器(filter)的形式实现的。这一过滤器应该截取全部的对 Web 应用的请求,而且也应该在诸多过滤器组成的链中排在第一个。Spring Session 的过滤器会负责确保全部后续的代码里面对 javax.servlet.http.HttpServletRequest.getSession()
方法的调用都会呈递给一个 Spinrg Session 的 HttpSession
实例,而不是应用服务器默认提供的 HttpSession
。
要理解这点,最简单的方法就是查阅 Spring Session 的实际源码。咱们首先从用来实现 Spring Session 的标准 Servlet 扩展点(extension points)开始。
在 2001 年,Servlet 2.3 规范引入了 ServletRequestWrapper。该类的 Javadoc 称 ServletRequestWrapper “为 ServletRequest 接口能让开发者继承它来适配一种特别的 Servlet 提供了一种便利的实现。该类采用了包装器,或者说装饰器模式。对该类的 ServletRequest 类的方法的调用会被传至其封装的一个请求对象里去。” 下面这段从 Tomcat 里抽出来的代码就展现了 ServletRequestWrapper 的实现方式。
public class ServletRequestWrapper implements ServletRequest {
private ServletRequest request;
/** * Creates a ServletRequest adaptor wrapping the given request object. * 建立一个装有给定的请求对象的 ServletRequest 适配器 * @throws java.lang.IllegalArgumentException if the request is null * 若是请求对象为空就会抛出空指针异常 */
public ServletRequestWrapper(ServletRequest request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
this.request = request;
}
public ServletRequest getRequest() {
return this.request;
}
public Object getAttribute(String name) {
return this.request.getAttribute(name);
}
// 为可读性着想, 接下来的代码就略了
}
复制代码
Servlt 2.3 规范还对 ServletRequestWrapper
定义了一个子类 HttpServletRequestWrapper
。咱们能够用它来快速地实现一个自定义的 HttpServletRequest
。下面这段从 Tomcat 里抽出来的代码就展现了 HttpServletRequestWrapper
这个类的实现方式。
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
public HttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest) super.getRequest();
}
public HttpSession getSession(boolean create) {
return this._getHttpServletRequest().getSession(create);
}
public HttpSession getSession() {
return this._getHttpServletRequest().getSession();
}
// 为可读性着想,接下来的代码就略了
}
复制代码
所以,咱们就能够用这些包装类来编写一些扩展 HttpServletRequest 功能的代码,重载返回 HttpSession 的方法,使得后者返回的是咱们存储在外部存储仓库里面的会话。这里就给出一份从 Spring Session 项目提出来的源码就对应了这里提到的东西。为了能对应这里的解释,源码里面本来的注释被我重写了一下,在此不妨也看一看里面的注释。
/* * Spring Session 项目定义了一个继承了标准 HttpServletRequestWrapper 的类 * 它重载了 HttpServletRequest 里面的全部跟会话有关的方法 */
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
private HttpSessionWrapper currentSession;
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
private final HttpServletResponse response;
private final ServletContext servletContext;
/* * 构造方法这块很是简单 * 它会接收并设置一些以后会用到的参数, * 而后完成对 HttpServletRequestWrapper 的代理 */
private SessionRepositoryRequestWrapper(
HttpServletRequest request,
HttpServletResponse response,
ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
/* * Spring Session 便在这里用本身对返回存储于外部数据源的会话数据的实现 * 取代了对应用服务器提供的默认方法的代理调用. * * 这里的实现会先检查它是否是已经有一个对应的会话. * 如有那就返回之, 不然就会检查当前的请求附带的会话 ID 是否确实对应着一个会话 * 如有, 那就用这个会话 ID 从 SessionRepository 里边加载这个会话; * 若外部数据源里没这个会话, 或者这个会话 ID 没对应的会话, * 那就建立一个新的会话, 并把它存在会话数据存储里面. */
@Override
public HttpSession getSession(boolean create) {
if(currentSession != null) {
return currentSession;
}
String requestedSessionId = getRequestedSessionId();
if(requestedSessionId != null) {
S session = sessionRepository.getSession(requestedSessionId);
if(session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
return currentSession;
}
}
if(!create) {
return null;
}
S session = sessionRepository.createSession();
currentSession = new HttpSessionWrapper(session, getServletContext());
return currentSession;
}
@Override
public HttpSession getSession() {
return getSession(true);
}
}
复制代码
Spring Session 同时定义了一个 ServletFilter
接口的实现类 SessionRepositoryFilter
。这里也会给出这个过滤器的实现的核心部分的源码,而且也会附上一些对应本文内容的注释,不妨也看一看。
/* * SessionRepositoryFilter 是一个标准 ServletFilter 的实现. * 其目的是从它的基类扩展出一些功能来. */
public class SessionRepositoryFilter < S extends ExpiringSession > extends OncePerRequestFilter {
/* * 这一方法就是核心部分. * 该方法会建立一个咱们在上面介绍过的包装请求的实例, * 而后拿这个包装过的请求再过一遍过滤器链的剩余部分. * 关键的地方在于,应用在执行位于这个过滤器以后的代码时, * 若是要获取会话的数据, 那这个包装过的请求就会返回 Spring Session * 所保存在外部数据源的 HttpServletSession 实例. */
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest =
new SessionRepositoryRequestWrapper(request,response,servletContext);
SessionRepositoryResponseWrapper wrappedResponse =
new SessionRepositoryResponseWrapper(wrappedRequest, response);
HttpServletRequest strategyRequest =
httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse =
httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
}
}
复制代码
这一节的重点在于,基于 HTTP 的 Spring Session 其实也只是一个用了 Servlet 规范的标准特性来实现功能的经典的 Servlet 过滤器而已。所以,将现有的 Web 应用的 war 文件改为使用 Spring Session 是应该能够不用改动已有代码的。然而,在应用里面用了 javax.servlet.http.HttpSessionListener
的状况则是例外。Spring Session 1.0 并无对 HttpSessionListener
提供支持,不过 Spring Session 1.1 M1 版本则对其添加了支持。详情见此。
在 Web 项目里面,Spring Session 的设置分为四步:
Spring Session 内置了对 Redis 的支持。安装和设置 redis 的详细信息见此。
完成上述 Spring Session 的设置步骤的常见方式有两种。一种是使用 Spring Boot 来自动设置 Spring Session。另一种则是手动完成每个配置步骤。
用 Maven 和 Gradle 等依赖管理工具能够很轻松地将 Spring Session 加入到应用的依赖项目里面。好比说,若是你用的是 Spring Boot + Maven,那么就能够在 pom.xml 里面加上如下依赖项目:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
复制代码
spring-boot-starter-redis
这一依赖项目会确保跟 redis 交互所需的 jar 包都包含在应用里面,这样即可以使用 Spring Boot 来进行自动的配置。至于 spring-session
这一依赖项目则对应 Spring Session 的 jar 包。
设置 Spring Session Servlet 过滤器的过程能够经过 Spring Boot 自动完成,只须要在 Spring Boot 的配置类里面加上 @EnableRedisHttpSession
注解便可。就跟下面这段代码同样:
@SpringBootApplication
@EnableRedisHttpSession
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
复制代码
将下面这些配置信息加到 Spring Boot 的 application.properties 文件便可设置 Spring Session 到 Redis 的链接。
spring.redis.host=localhost
spring.redis.password=secret
spring.redis.port=6379
复制代码
为了设置和 Redis 的链接,Spring Boot 提供了一套详实的底层架构,使得咱们能够在其中任意设置一种跟 Redis 创建链接的方式。你能在 Spring Session 还有 Spring Boot 里面找到循序渐进进行的指南。
使用 web.xml 来设置传统的 Web 应用去使用 Spring Session 的教程见此。
设置传统的不带有 web.xml 的 war 文件去使用 Spring Session 的教程见此。
在默认状况下,Spring Session 会使用 HTTP cookie 来存储会话 ID,可是咱们也能够将 Spring Session 设置成使用自定义的 HTTP 报文首部字段(例如 x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3
)来存储会话 ID,而这在构建 RESTful API 的时候会很是有用。完整教程见此。
在配置了 Spring Session 以后,咱们就可使用标准的 Servlet API 去和它进行交互了。好比下面这段代码就定义了一个使用标准 Servlet 会话 API 来访问会话数据的 servlet。
@WebServlet("/example")
public class Example extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 使用标准的 servlet API 去获取对应的会话数据
// 这一会话数据就是 Spring Session 存在 Redis
// 或是别的咱们所指定的数据源里面的会话数据
HttpSession session = request.getSession();
String value = session.getAttribute(“someAttributeâ€);
}
}
复制代码
Spring Session 经过使用一个叫作 _s
的会话代号参数来跟踪每一个用户的多个会话。假若有个传入请求的 URL 是 http://example.com/doSomething?_s=0
,那么 Spring Session 就会读取 _s
参数的值,而后便会认为这个请求对应的是默认的会话。
若是传入请求的 URL 是 http://example.com/doSomething?_s=1
,那么 Spring Session 就会知道这个请求对应的会话的代号是 1。若是传入请求没有指定参数 _s
,那么 Spring Session 就会把它视为对应默认对话(即 _s = 0
)。
为了让每一个浏览器都建立一个新的会话,咱们只需像之前那样调用 javax.servlet.http.HttpServletRequest.getSession()
,而后 Spring Session 就会返回对应的会话,或者使用 Servlet 规范的语义建立一个新的会话。下表便给出了 getSession()
方法在同一浏览器的不一样的 URL 参数下的具体表现形式:
HTTP 请求 URL | 会话代号 | getSession() 的具体表现 |
---|---|---|
example.com/resource | 0 | 若是存在与代号 0 相关联的会话就返回之,不然就建立一个新会话,而后将其与代号 0 关联起来 |
example.com/resource?_s=1 | 1 | 若是存在与代号 1 相关联的会话就返回之,不然就建立一个新会话,而后将其与代号 1 关联起来 |
example.com/resource?_s=0 | 0 | 若是存在与代号 0 相关联的会话就返回之,不然就建立一个新会话,而后将其与代号 0 关联起来 |
example.com/resource?_s=abc | abc | 若是存在与代号 abc 相关联的会话就返回之,不然就建立一个新会话,而后将其与代号 abc 关联起来 |
如上表所示,会话代号并不局限于整数,只要与发布给该用户的全部其余会话别名不一样,便可对一个一个新的会话。然而,整数类型的会话代号应该是最易用的,而且 Spring Session 也给出了 HttpSessionManager
来提供一些处理会话代号的实用方法。
咱们能够经过 "org.springframework.session.web.HttpSessionManager"
这个属性名来查找相应属性,进而访问到 HttpSessionManager
。下面这段代码就演示了得到 HttpSessionManager
的引用的方法,以及这个实用方法类的一些主要的方法。
@WebServlet("/example")
public class Example extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,HttpServletResponse response)
throws ServletException, IOException {
/* * 经过使用 "org.springframework.session.web.http.HttpSessionManager" * 这一属性名在请求属性中查找属性 * 来获取一个 Spring Session 的 HttpSessionManager 的引用 */
HttpSessionManager sessionManager=(HttpSessionManager)request.getAttribute(
"org.springframework.session.web.http.HttpSessionManager");
/* * 用 HttpSessionManager 来找出 HTTP 请求所对应的会话代号. * 默认状况下这个会话代号会由 HTTP 请求的 URL 参数 _s 给出。 * 好比 http://localhost:8080/example?_s=1 这个 URL * 就会让这里的 println() 方法打印 "Requested Session Alias is: 1" */
String requestedSessionAlias=sessionManager.getCurrentSessionAlias(request);
System.out.println("Requested Session Alias is: " + requestedSessionAlias);
/* * 返回一个当前还没被浏览器用在请求参数里的惟一的会话代号. * 注意这一方法并不会建立一个新的会话, * 建立新的会话仍是要经过 request.getSession() 来进行. */
String newSessionAlias = sessionManager.getNewSessionAlias(request);
/* * 使用刚刚获得的新会话代号构造一个 URL, * 使其含有 _s 这个参数. * 好比若 newSessionAlias 的值是 2, * 那么这个方法就会返回 "/inbox?_s=3" */
String encodedURL = sessionManager.encodeURL("/inbox", newSessionAlias);
System.out.println(encodedURL);
/* * 返回一个会话代号为键, 会话 ID 为值的 Map, * 以便识别浏览器发来的请求所对应的会话. */
Map <String, String> sessionIds = sessionManager.getSessionIds(request);
}
}
复制代码
Spring Session 为企业级 Java 应用的会话管理领域带来了革新,让咱们能够轻松作到:
若你在寻找一种从传统又笨重的应用服务器中解放的方法,但又囿于对应用服务器的会话存储集群功能的依赖,那么 Spring Session 对像 Tomcat、Jetty 还有 Undertow 这样的容器的轻量化来讲是很好的一个选择。
Spring Session 教程及指南
Websocket / HttpSession 超时交互
网络研讨会:Spring Session 导论
问答
传统Web应用程序和API中的身份验证、受权和会话管理如何实现?
相关阅读
此文已由做者受权腾讯云+社区发布,原文连接:https://cloud.tencent.com/developer/article/1151972?fromSource=waitui
欢迎你们前往腾讯云+社区或关注云加社区微信公众号(QcloudCommunity),第一时间获取更多海量技术实践干货哦~