spring boot项目17:session

JAVA 8

Spring Boot 2.5.3

Google Chrome 92+

---

 

授人以渔:

1、Spring Boot Reference Documentation

This document is also available as Multi-page HTML, Single page HTML and PDF.

有PDF版本哦,下载下来!

2、Spring Session

Spring Session provides an API and implementations for managing a user’s session information.

PDF版本哦,下载下来!

 

项目:web0920(Web项目)

 

目录

1、普通Web项目的Session

请求中的session调试

Session及Cookie配置

session存储到本地

2、Security的Web项目的Session

3、使用Spring Session

试验1:使用Redis作为session存储

试验2:多应用共享Session

参考文档

 

1、普通Web项目的Session

建立项目,依赖:spring-boot-starter-web

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

开发端点/home/get:

@RestController
@RequestMapping(value="/home")
@SpringBootApplication
public class Web0920Application {

	// 注入请求对象
    @Autowired
	private HttpServletRequest req;
    
    // 新增端点:输出请求、请求中的session信息
	@GetMapping(value="/get")
	public String get() {
		cs.accept("req=" + req);
		cs.accept("req=" + req.getClass());
        
		HttpSession hsess = req.getSession();
		cs.accept("req sess=" + hsess);
        
		String sid = req.getRequestedSessionId();
		cs.accept("req sid=" + sid);
        
		cs.accept("req=" + req.getSession(true));
        
		sid = req.getRequestedSessionId();
		cs.accept("req sid=" + sid);
		
		return new Date().toString();
    }

}

调用/home/get端点:

- 在Chrome中按下 F12,进入 Application->Storage->Cookies,可以看到其中存在一个Cookie,名称为JSESSION

- 检查应用的日志

请求req的类型 是 动态代理类;

req.getSession() 返回HttpSession,类型为 org.apache.catalina.session.StandardSessionFacade,位于 tomcat-embed-core-x.x.x.jar包中,这样来看,SESSION是由嵌入式Tomcat管理的;

执行 req.getSession(true) 前后,SessionId 发生了变化,最后在浏览器上展示的是 新建的session id——Cookie JSESSION的值;

req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy50
req sess=org.apache.catalina.session.StandardSessionFacade@2f463540
req sid=C05EFABC37A3F027E64F7F63A2006FFE
req=org.apache.catalina.session.StandardSessionFacade@2f463540
req sid=C05EFABC37A3F027E64F7F63A2006FFE
req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy50
req sess=org.apache.catalina.session.StandardSessionFacade@2f463540
req sid=D48B21C607B1B98A9BBFCDD990278732
req=org.apache.catalina.session.StandardSessionFacade@2f463540
req sid=D48B21C607B1B98A9BBFCDD990278732

补充说明:

清理掉浏览器的cookie时,上面请求中的req sid=null;但是,返回到浏览器时,又生成了Cookie JSESSION。这样看来,SESSION是在端点返回时生成的?下面调试时会介绍。

测试代码及执行日志:

端点代码:
	@GetMapping(value="/get")
	public String get() {
		cs.accept("req=" + req);
		cs.accept("req=" + req.getClass());
		
		HttpSession hsess = req.getSession();
		cs.accept("req sess=" + hsess);
		
		String sid = req.getRequestedSessionId();
		cs.accept("req sid 1=" + sid);
		
		sid = req.getRequestedSessionId();
		cs.accept("req sid 2=" + sid);
		
		return new Date().toString();
	}

清理浏览器中的cookie后,执行结果日志:
req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy50
req sess=org.apache.catalina.session.StandardSessionFacade@5f0de3eb
req sid 1=null
req sid 2=null

此时,浏览器得到的Cookie JSESSION = 636A9BB7D3E507E0D2555F3A60877D53

再次访问端点,执行结果日志:
req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy50
req sess=org.apache.catalina.session.StandardSessionFacade@5f0de3eb
req sid 1=636A9BB7D3E507E0D2555F3A60877D53
req sid 2=636A9BB7D3E507E0D2555F3A60877D53

此后多次执行,Cookie JSESSION不变。

 

请求中的session调试

session是怎么产生的呢?调试(DEBUG)可知。

在嵌入式Tomcat的包中,有一个 org.apache.catalina.connector.Request 类,

package org.apache.catalina.connector;
...
public class Request implements HttpServletRequest {

    @Override
    public HttpSession getSession() {
        Session session = doGetSession(true);
        if (session == null) {
            return null;
        }

        return session.getSession();
    }
    
    @Override
    public HttpSession getSession(boolean create) {
        Session session = doGetSession(create);
        if (session == null) {
            return null;
        }

        return session.getSession();
    }
}

其中有两个getSession函数,在第二个有参数的 getSession(boolean create) 中添加断点——doGetSession,再执行调试(调试前先清理浏览器的cookie),可以看到 session 在一个请求中新建的过程:

在 org.apache.catalina.session.ManagerBase 中 有一个 createSession(String sessionId) 函数,继续调试,调用 generateSessionId() 函数时产生了 sessionID。

浏览器得到响应后的Cookie值:和上面创建的相同

继续调试,进入了 org.apache.catalina.connector.CoyoteAdapter 的 postParseRequest函数:

其中有一个 SessionConfig 类 可以看下,

CoyoteAdapter 中的parseSessionCookiesId函数会 获取请求的Cookies,其中就包含JSESSIONID的:

调试到代码中调用 req.getRequestedSessionId() 的地方:

通过上面的调试,可以看到在Web请求中Session的创建、获取过程。

 

疑问:嵌入式的Tomcat是这么管理session的,Jetty、Undertow的呢?

 

Session及Cookie配置

在本文的Web应用(Embedded Servlet Container)中,Session、Cookie是可以配置的。

官文中展示了下面的配置:

server.servlet.session.persistent

Whether to persist session data between restarts.

server.servlet.session.store-dir

Directory used to store session data.

server.servlet.session.timeout

server.servlet.session.cookie.* // cookie的配置

……

 

HttpSession对象 是有很多方法可以调用的:

调用下面的程序测试:

	@GetMapping(value="/get")
	public String get() {
		cs.accept("\nreq=" + req);
		cs.accept("req=" + req.getClass());

		// 请求中的session id:旧值
		String sid = req.getRequestedSessionId();
		cs.accept("req sid 1=" + sid);
		
		// 请求处理后的,新值
		HttpSession hsess = req.getSession();
		cs.accept("req sess=" + hsess);
		cs.accept("createTime=" + new Date(hsess.getCreationTime()));
		cs.accept("LastAccessedTime=" + new Date(hsess.getLastAccessedTime()));
		cs.accept("MaxInactiveInterval=" + hsess.getMaxInactiveInterval() + "秒");
		
		cs.accept("hsess.getId()=" + hsess.getId());
		cs.accept("hsess.isNew()=" + hsess.isNew());
		
		return new Date().toString();
	}

原来,req.getRequestedSessionId() 获取的是 请求中的session——根据cookie解析得来,难怪之前会显示null!

而req.getSession()获取的HttpSession hsess对象 是请求中的 实时值。

这样的话,前面的一个介绍是错误的——session 其实是在请求处理过程中新建的,而不是 端点处理完后,返回过程中创建。纠正!

执行结果:

# 浏览器有 旧cookie时
req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy50
req sid 1=5307455366ED81818A4BF23DF1B60D87
req sess=org.apache.catalina.session.StandardSessionFacade@4da148de
createTime=Wed Sep 22 22:56:10 CST 2021
LastAccessedTime=Wed Sep 22 22:56:10 CST 2021
MaxInactiveInterval=1800秒
hsess.getId()=128190BB325A6235595E9292BCDEF464
hsess.isNew()=true

# 注意 最后一行的 isNew()=false 不是新的了
req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy50
req sid 1=128190BB325A6235595E9292BCDEF464
req sess=org.apache.catalina.session.StandardSessionFacade@4da148de
createTime=Wed Sep 22 22:56:10 CST 2021
LastAccessedTime=Wed Sep 22 22:56:10 CST 2021
MaxInactiveInterval=1800秒
hsess.getId()=128190BB325A6235595E9292BCDEF464
hsess.isNew()=false

 

配置session及cookie:

# session超时配置:默认30m
server.servlet.session.timeout=60s

# cookie配置
server.servlet.session.cookie.name=mysession
server.servlet.session.cookie.max-age=60s

 

配置前的Cookie样式:

配置后的Cookie:可以看到多了一个名为mysesion的cookie,其属性也和配置一致(官文中还有更多配置)。

若是清理了cookie再访问,就没有旧的JSESSIONID了。

检查日志:session 的 MaxInactiveInterval 变成 60秒了,也就是说 最少60秒更新一次——只要有请求进来。

req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy52
req sid 1=null
req sess=org.apache.catalina.session.StandardSessionFacade@5eb81a04
createTime=Wed Sep 22 23:08:11 CST 2021
LastAccessedTime=Wed Sep 22 23:08:11 CST 2021
MaxInactiveInterval=60秒
hsess.getId()=7AB5481E512D71C5EC2520A0F39E6E0D
hsess.isNew()=true

req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy52
req sid 1=7AB5481E512D71C5EC2520A0F39E6E0D
req sess=org.apache.catalina.session.StandardSessionFacade@5eb81a04
createTime=Wed Sep 22 23:08:11 CST 2021
LastAccessedTime=Wed Sep 22 23:08:11 CST 2021
MaxInactiveInterval=60秒
hsess.getId()=7AB5481E512D71C5EC2520A0F39E6E0D
hsess.isNew()=false

更多配置,待解锁。

 

思考:Web应用的需要session,Reactive Web需要吗?

 

session存储到本地

添加下面的配置:

server.servlet.session.persistent=true
# 错误的地址
#server.servlet.session.store-dir=D:\data\session\web0920
# 正确的配置
server.servlet.session.store-dir=/data/session/web0920

启动应用,此时,目录为空。

访问/home/get端点:

在项目web0920的根目录下出现了 /data/session/web0920目录,访问期间,其下没有内容。

关闭应用,此时出现一个SESSIONS.ser文件:

前面步骤浏览器中记录的session值:C232AABAE7917EE147F6CB18945C5170 ,有效期1800秒

再次重启应用,看看浏览器的这个session是否继续有效——访问时不会建新的?旧的仍然有效,不会生成新的!

只不过,应用启动后,/data/session/web0920 下的文件没了:来自博客园

疑问

1、正常停止程序的时候 会来得及保存session,要是程序异常关闭了呢——更严重的导致关闭的原因,比如断电?

2、怎么设置保存到 某个指定目录,而不是 项目根目录下呢?

 

session虽然存储到了本地,但从前面的测试来看,这种方式的session也是无法在 多个项目中共享的。

要实现session在多个应用的共享,需要用到Spring Session技术,第三章介绍。来自博客园

 

2、Security的Web项目的Session

在第一章的基础上,添加依赖包:spring-boot-starter-security

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

启动项目:启动后,存在一个DefaultSecurityFilterChain,其中包含一个 SessionManagementFilter对象,这个 SessionManagementFilter有什么用呢?

访问前面的 /home/get,跳转到登录页,使用 user + 启动日志中的密码 登录,此时,/home/get端点输出:

req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy53
req sid 1=EBBF5F23FE2D77D5BCE3860A998D80A9
req sess=org.apache.catalina.session.StandardSessionFacade@3c17eb29
createTime=Wed Sep 22 23:16:07 CST 2021
LastAccessedTime=Wed Sep 22 23:16:32 CST 2021
MaxInactiveInterval=1800秒
hsess.getId()=EBBF5F23FE2D77D5BCE3860A998D80A9
hsess.isNew()=false

打开登录页面时,显示的是SESSION-A,点击登录,调用/login 端点,此时,请求中的cookie为SESSION-A,登录成功,生产了新的SESSION-B

之后访问 /home/get 时,都是使用的 SESSION-B 了。来自博客园

 

SESSION-B 是怎么产生的?难道是前面的 SessionManagementFilter中?

调试 SessionManagementFilter 的 doFilter 应该可以知道答案。

结论:

调试上面的 doFilter,和 session的创建没有关系,还是要调试 前面的Request的getSession函数才可以一步一步找到。

SessionManagementFilter 到底有什么用呢?在安全中可以发挥什么作用?TODO

 

存在一个 SessionAuthenticationStrategy接口:其中的 onAuthentication 方法 是用来做安全处理的?

public interface SessionAuthenticationStrategy {

	/**
	 * Performs Http session-related functionality when a new authentication occurs.
	 * @throws SessionAuthenticationException if it is decided that the authentication is
	 * not allowed for the session. This will typically be because the user has too many
	 * sessions open at once.
	 */
	void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
			throws SessionAuthenticationException;

}

 

---210922 2357--

 

3、使用Spring Session

前面的应用中,session都是有应用自行管理的,无法在应用间共享、也无法持久化。

session无法共享,会导致依赖session存储数据的服务无法轻易地横向扩展,会导致端点提供的服务是 有状态服务。

session无法持久化,会导致数据丢失,比如,用户登录后,但应用重启了,此时,用户就需要再次登录——因为session没了。

因此,Spring框架提供了Spring Session,Spring Boot提供了它的自动配置(auto-configuration)

对于 Servlet web application(本文使用的项目),提供了下面的存储方式:

• JDBC
• Redis
• Hazelcast
• MongoDB

而对于reactive web application,Spring Session提供了两种存储方式:来自博客园

• Redis
• MongoDB

 

依赖包:spring-session-core

<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-core</artifactId>
</dependency>

对应 https://start.spring.io/ 的 Spring Session:

如果在建立项目时,选择了Spring Session、Spring Data Redis,此时的依赖包会有两个:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>

其中,spring-session-data-redis 结果如下:包含了前面的 spring-session-core

疑问

在上面选择了Redis、Session后,再选择JDBC相关包会怎样?来自博客园

官文的说法,

如果只有单个的Spring Session module在classpath中,S.B.自动选择存储实现方式。

要是有多个存储实现方式,就需要配置StoreType,比如,下面配置为 jdbc:

spring.session.store-type=jdbc

设置为none则会禁止Spring Session。

 

每次存储方式都有额外的配置,可以看S.B.的官文了解:spring.session.*、spring.session.jdbc.、spring.session.mongodb.、spring.session.hazelcast.、spring.session.redis.。

 

对于spring session的配置,还可以使用 @Enable*Session 相关注解。下面是使用Redis作为实现时找到的4个@Enable*注解:来自博客园

HttpSession结尾的是 Servlet Web,而 WebSession结尾的是 Reactive Web应用的。

这个方式的配置会导致S.B.的自动配置、前面的spring.session.* 配置优先级降低。

 

还有一个 spring.session.timeout 属性,在 Servlet Web应用中,如果没有的话,就会使用之前的 server.servlet.session.timeout。

 

试验1:使用Redis作为session存储

引入依赖包(见上文):spring-boot-starter-data-redis、spring-session-data-redis

添加Redis配置:

#
# Redis
# mylinux 是虚拟机的本地域名,配置到 hosts文件中
spring.redis.host=mylinux
spring.redis.port=6379

启动项目:成功。

检查redis服务器中的键:没有session相关的。来自博客园

127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x05test1"
2) "\xac\xed\x00\x05t\x00\x04set1"
3) "\xac\xed\x00\x05t\x00\x05test3"
127.0.0.1:6379>

访问/home/get端点:成功。但是,此时浏览器有一个name=SESSION的cookie——而不是 JSESSION。

再次检查Redis:多了很多 spring:session 开头的键。

127.0.0.1:6379> keys *
1) "spring:session:sessions:expires:a51d5f9f-5e70-4797-a5e6-e9f42eea0008"
2) "spring:session:sessions:6978552e-8f6b-4bb3-903c-9d3f56d8dcd0"
3) "spring:session:sessions:expires:6978552e-8f6b-4bb3-903c-9d3f56d8dcd0"
4) "spring:session:expirations:1632369240000"
6) "spring:session:sessions:a51d5f9f-5e70-4797-a5e6-e9f42eea0008"
8) "spring:session:expirations:1632369120000"
127.0.0.1:6379>

 

重启项目,检查在session有效期内,是否需要重新生成新session?结果是,不需要!符合预期。

试验2:多应用共享Session

需要结合Nginx使用——转发请求到多个应用。来自博客园

两个应用:8080、9090端口。

配置hosts文件:后面都访问 session.com

127.0.0.1 session.com

配置Nginx转发请求 session.com的请求到 两个应用:

# upstreams\session.conf文件
upstream session_com {
	server localhost:8080;
	server localhost:9090;
}

# servers\session.conf文件
server {
	listen       80;
	server_name  session.com;

	sendfile        on;

	access_log  logs/xq.access.log main;

	location / {
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   Host      $http_host;
        proxy_set_header X-NginX-Proxy true;

		proxy_pass http://session_com;
	}
}

 

测试多次访问 http://session.com/home/get:来自博客园

请求成功。并且,由两个应用均衡地处理请求。并且,实现了session共享。

 

奇怪的是,Cookie的值 和 req.getRequestedSessionId()、缓存中的值 不一样

日志 和 redis-cli中:

日志:
req=Current HttpServletRequest
req=class com.sun.proxy.$Proxy62
req sid 1=2079631a-8efb-4a17-860d-9e729b0c31a8
req sess=org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper@6e2d8002
createTime=Thu Sep 23 11:33:25 CST 2021
LastAccessedTime=Thu Sep 23 11:35:54 CST 2021
MaxInactiveInterval=1800秒
hsess.getId()=2079631a-8efb-4a17-860d-9e729b0c31a8
hsess.isNew()=false

redis-cli:部分
127.0.0.1:6379> keys *
 6) "spring:session:sessions:expires:2079631a-8efb-4a17-860d-9e729b0c31a8"
 7) "spring:session:sessions:2079631a-8efb-4a17-860d-9e729b0c31a8"
127.0.0.1:6379>

在这个过程中发生了什么?TODO

 

本文还有更多内容需要进一步学习:

1、spring.session.redis.*的配置

2、@EnableRedisHttpSession的配置

3、Cookie的设置呢?

4、Spring Session + Spring Security

以便得到spring session的最佳实践。

 

》》》全文完《《《来自博客园

 

Spring Session在大规模系统中用的多吗?

在本文的项目中用起来还行,可是,存在什么隐患?要怎么研究?

Session+Security 不算是主流解决方案了吧?Oauth2什么的才是吧?

需要更多了解。来自博客园

 

---210923 1149---

 

参考文档

1、Spring Security Web 5.1.2 源码解析 -- SessionManagementFilter

2、

posted @ 2021-09-23 11:50  快乐的总统95  阅读(744)  评论(0编辑  收藏  举报