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版本哦,下载下来!
Spring Session provides an API and implementations for managing a user’s session information.
有PDF版本哦,下载下来!
项目:web0920(Web项目)
目录
建立项目,依赖: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是怎么产生的呢?调试(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的呢?
在本文的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需要吗?
添加下面的配置:
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技术,第三章介绍。来自博客园
在第一章的基础上,添加依赖包: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--
前面的应用中,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。
引入依赖包(见上文):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?结果是,不需要!符合预期。
需要结合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、