从底层说起,如何使用Spring Session

引言

说到Java Web开发,就一定会涉及到用户会话的状态。然而,http却是无状态的(关于如何理解http是无状态的,可以看这里),为了管理用户的会话,我们发明了Cookie-Session机制。

本篇文章共分为三个部分。第一个部分主要会解释Cookie-Session这两个名词的定义以及基本实现。第二部分则会给大家介绍Tomcat是如何处理Cookie和Session的。第三部分则会讲如何在Spring中使用自定义的session,包括对其中的涉及的设计模式分析。

Cookie与Session的定义

Cookie 与 Set-Cookie

RFC6265给出了Cookie与Set-Cookie的具体定义。

Cookie与Set-Cookie都是Http Header的一种。通过Set-Cookie这个Http Header,服务器能够传递一个键值对以及这个键值对所包含的元数据给客户端(通常指浏览器),而客户端在后续的请求中,也应当将根据原数据及其他信息决定是否需要将这个键值对返回给服务器。

举个例子,我们常常会在某些网站上进行语言选择,而一旦我们选择语言之后,即便我们再次关闭浏览器重新打开,也不再需要重新选择语言,就是通过这种机制在发生作用,具体流程如下:

首先,用户点击了语言选择,服务器向浏览器发送响应中增加一个http header Set Cookie(若键值对存在原数据,这部分原数据也会一并被发送)

== Server -> User Agent ==

Set-Cookie: lang=en-US; Max-Age=1800

随后,用户浏览其它网页,在浏览其它网页的过程中,浏览器向服务器发送的请求中,会一直在http header的Cookie中把lang=en-US这个键值对带上(键值对的原数据不会再发送给服务器)。

== User Agent -> Server ==

Cookie: lang=en-US

以上这个示例,就是Set-CookieCookie的作用机制。笔者还提到了在Set-CookieCookie中传递的键值对是拥有一些原数据的,也就是它们的一些属性,部分常用属性列表如下所示:

属性名用途示例
Expires键值对什么时候会过期(若键值对已过期,浏览器在进行请求的时候就不会再携带这个键值对)Set-Cookie: lang=en-US; Expires=Sun, 06 Nov 1994 08:49:37 GMT
Max-Age同样表示键值对什么时候会过期,优先级高于Expires,若未指定或者小于0,则代表Cookie在窗口关闭之后实效Set-Cookie: lang=en-US; Max-Age=1800
Domain键值对可以作用于哪些域名Set-Cookie: lang=en-US; Domain=example.com
Path键值对作用于哪些请求路径Set-Cookie: lang=en-US; Path=/

Cookie与Session

了解完CookieSet-Cookie之后,我们就知道服务器与客户端是如何通过无状态的http协议来传递用户状态的了。但是Cookie也存在着它的局限,如http协议下明文传输,最多只能传递4KB的内容。

所以,目前的服务器大部分都是只在Cookie中传递一个key,通过key在服务器中获取更多关于会话的内容,而这些内容,我们统称为Session

如果我们使用的是tomcat服务器,可以在服务接口中调用如下代码:

        HttpSession session = request.getSession();
        System.out.println(session.getAttribute("test"));
        session.setAttribute("test", "test");

当我们首次通过浏览器调用这个接口的时候,我们就能够在响应头中看到以下内容:

Set-Cookie: JSESSIONID=421382EDF30D3BAF0EF36B4C3FC8F303; Path=/; HttpOnly

此时,控制台中会输出null

而在我们下一次通过浏览器请求同样的接口的时候,就能发现,Cookie也携带了相同的内容

Cookie: JSESSIONID=421382EDF30D3BAF0EF36B4C3FC8F303

而同时,控制台中会输出test。这就是Cookie与Session的作用机制。服务器通过Set-Cookie将SessionId传递给客户端,客户端通过Cookie回传,服务端在取得SessionId之后,根据这个SessionId去查找存储在后端的Session对象。

Tomcat中对Cookie与Session的处理

在了解tomcat中对Cookie与Session的处理之前,建议大家首先了解一下tomcat的整体设计以及架构,可以参考这里

简单的流程如下所示:
简单的Spring Session流程图

从这里我们大概能看到,tomcat中的处理,实际上是将http请求中的cookie里面的JESSIONID解析为Request的一个属性,而Session真正的创建与获取,都发生在request.getSession()这个方法被调用的时候。

在这里我们也能够发现,如果我们需要自定义一个管理器的话,是可以通过指定tomcat中的context里面的Manager来实现的,但是直接实现Manager的成本太高(定义的接口数量太多),而抽象类ManagerBase中实际上已经实现了通过ConcurrentHashMap管理Session的所有功能,也并未指明如果实现定制化的session管理,可以通过重写哪些方法来达到。因此,就有了Spring-Session。

通过Spring Session 快速实现Session管理

类图

想要了解Spring Session是如何实现Session管理的,我们需要了解一下其类图。

Spring Session类图

通过这个类图我们可以发现,Spring使用了桥接模式,通过HttpSessionWrapper将两种Session结合在了一起。

那么接下来,我们看看它是如何工作的。主要是通过SessionRepositoryFilter这个Filter来进行实现的。

protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
				request, response, this.servletContext); //将request wrap成特定的request
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
				wrappedRequest, response); //将response wrap成特定的response

		HttpServletRequest strategyRequest = this.httpSessionStrategy
				.wrapRequest(wrappedRequest, wrappedResponse);
		HttpServletResponse strategyResponse = this.httpSessionStrategy
				.wrapResponse(wrappedRequest, wrappedResponse);

		try {
			filterChain.doFilter(strategyRequest, strategyResponse); //继续原有逻辑
		}
		finally {
			wrappedRequest.commitSession();
		}
	}

实际上也就是通过桥接模式完成了对象的转换,然后再继续原有的逻辑,因为对Session做了转换,因此这个filter的顺序要排在所有其它可能会用到session或者会提交响应的filter前面,安全起见最好是设置最高的优先级。

而这个类在初始化的时候,必须要指定SessionRepository,也就是控制Spring Session存储的类。这个类需要实现的接口如下:

  //创建Session,这里的Session也并不是HttpSession,而可以是我们自己实现的类(必须实现HttpSession接口),sessionId的规则也可以由我们自己定义
	S createSession();

	//将Session持久化,可以自定义持久化策略(本地Session/RedisSession/Mysql Session)
	void save(S session);

  // 根据ID获取Session
	S getSession(String id);

	//删除Session
	void delete(String id);

对比与Tomcat中的Manager接口,这里极大简化了自定义Session所需要的工作量。使得自定义Session更加方便。如果我们需要定制化更多的东西,如SessionId在Cookie中的名称,则可以参照这个Filter去实现更底层的逻辑。

最后,附上Spring Boot项目中,需要实现自定义Session管理的代码
Configuration.java spring boot 配置bean

	@Bean
    public SessionRepositoryFilter sessionRepositoryFilter(@Autowired CustomRedisSessionRepository customRedisSessionRepository) {
        SessionRepositoryFilter<MapSession> filter = new SessionRepositoryFilter<>(customRedisSessionRepository);
        return filter;
    }
	@Bean
    @Primary
    public FilterRegistrationBean sessionFilterBean(@Autowired SessionRepositoryFilter sessionRepositoryFilter) {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(sessionRepositoryFilter);
        registration.addUrlPatterns("/*");
        registration.setName("sessionFilter");
        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
        return registration;
    }   
    
     

CustomRedisSessionRepository.java

// 这里由于我们只需要管理session的存取,不需要定制化session,因此直接沿用了Srping 中定义的MapSession
public class CustomRedisSessionRepository implements SessionRepository<MapSession> {

    @Autowired
    private JedisService jediservice;

    private Gson jsonMapper = new Gson();
    
    @Autowired
    private UniqueID uniqueID; // 分布式系统的Unique Id组件

    @Override
    public MapSession createSession() {
        String sessionId = UUID.randomUUID().toString().concat("-").concat(Long.toString(uniqueID.getSequence().next()));  //在创建session的时候,可以自定义session格式,这里直接通过UUID + 分布式unique id中间件来避免分布式系统中的sessionId冲突问题
        return new MapSession(sessionId);
    }

    @Override
    public void save(MapSession session) {
        // 直接对session进行序列化并保存到redis中(比较粗暴的做法)
        jediservice.setex(RedisKeyConstants.getUserSessionKey(session.getId()), DynamicProperties.getUserAuthExpireSecs(), jsonMapper.toJson(session));
    }

    @Override
    public MapSession getSession(String id) {
        String res = jediservice.get(RedisKeyConstants.getUserSessionKey(id));
        if(StringUtils.isBlank(res)) {
            return null;
        }
        MapSession customRedisSession = jsonMapper.fromJson(res, MapSession.class);
        //取出的时候,需要判断是否过期了,如果过期了,需要将其删除
        if(customRedisSession.isExpired()) {
            delete(id);
            return null;
        }
        return customRedisSession;
    }

    @Override
    public void delete(String id) {
       jediservice.del(RedisKeyConstants.getUserSessionKey(id));
    }
}

以上可见,通过Spring Session,我们极大简化了自定义Session管理机制所需要的工作。只需要关心Session的存和取,不需要关心session的属性更新逻辑。

小结

关于Cookie和Session,从文档定义,到tomcat http解析处理,再到Spring中如何嵌套使用,这里都有比较详细的阐述。在阅读源码的过程中,可以加深对tomcat处理请求的理解,也能够看到Spring是如何把Session管理独立出来的。比如通过桥接模式,兼容了框架定义的org.springframework.session.Session的和Servlet中的javax.servlet.http.HttpSession。比如通过SessionRepository这个成员变量,将Session的持久化独立了出来,并且简化了其定义使得使用者更加容易能够对其进行扩展。

另外还有一个项目是Spring Redis Session,这个项目由于必须要配置指定的redis端口和地址,相对来说并没有那么灵活(如果项目中有涉及中间件的话,接入和改造成本都比较高),所以笔者最后没有选择这个方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值