开篇
web容器管理客户端与服务器的会话,核心机制就是让服务器为每个“陌生人”生成一张唯一编号的Session通行证,并借助Cookie让客户端自动携带这张通行证。 浏览器每次请求时递上编号,容器就能从自己的“档案室”里找到对应的Session数据,从而记住你是你是谁、之前聊到哪。
web容器怎么管理客户端与服务器的会话?从一次“握手”说起
想象你去一家咖啡馆,店员第一次见到你,递给你一张带号码的存包牌,你下次来的时候,出示牌子,店员就知道该取哪个包裹,web容器(比如Tomcat、Jetty)做的事儿,和那个店员一模一样。
客户端第一次请求时,容器在背后做了什么
假设你第一次访问一个Java Web应用,浏览器发出请求,web容器收到后,会按下面几步走:
- 检查请求里有没有携带名为
JSESSIONID的Cookie(或者其他会话标识)。 - 如果没有,就认为这是一个新访客,于是生成一个全球唯一的Session ID。
- 在服务器内存(或分布式存储)中创建一个Session对象,初始属性为空。
- 在响应头里通过
Set-Cookie把Session ID交给浏览器,浏览器会把它存在本地。
这个Session ID,就是那张存包牌上的号码,关键在于,号码本身不存储业务数据,数据都放在服务器端,容器内部维护着一个大Map,键是Session ID,值是对应的Session对象。
容器如何判断“这个人还是刚才那个人”
第二次请求来的时候,浏览器会自动带上上次拿到的Cookie,容器拿着Cookie里的Session ID,去自己的Session仓库里比对,找得到,就直接复用旧Session,让你接着上次的状态继续会话;找不到,就会判定会话过期或无效,重新走一遍创建流程。
这里面有个细节容易被忽略:web容器默认启用了Cookie作为首选识别方式,但Cookie在被禁用或者跨域时,容器会退而求其次,采用URL重写,把Session ID拼在链接后面,你可以去Tomcat的conf/context.xml里看,有个cookies属性,默认值是true。
session和cookie的区别是什么?一个存服务端,一个存浏览器
这个问题几乎是每个接触web开发的人都会卡壳的地方,行业共识认为,把Session和Cookie看作“表与里”的关系最直观:Cookie是浏览器端保存的“凭证”,Session是服务器端存储的“档案”。
| 对比项 | Session | Cookie |
|---|---|---|
| 存放位置 | 服务器内存或第三方存储 | 浏览器本地 |
| 数据大小 | 理论上可较大,但受内存约束 | 单条约4KB(各浏览器不同) |
| 生命周期 | 默认为会话期,超时或主动销毁 | 可通过Expires/Max-Age设置持久时间 |
| 安全性 | 数据在服务端,相对安全 | 明文传输,易被窃取 |
| 典型用途 | 用户登录态、购物车数据 | 记住用户名、埋点标识 |
Cookie的“小纸条”和Session的“档案室”
Cookie是浏览器手里攥着的一张小纸条,上面可能写着userName=frank或者theme=dark,服务器每次看到这张纸条,就能知道一些你的偏好,但它本身没能力确认你“登录过”,真正决定你是否登录的,是服务器档案室里的那份档案Session对象,Cookie里的Session ID,只是打开档案室的钥匙。
关闭浏览器,会话就消失了吗?
这里有个容易混淆的坑。关闭浏览器不等于销毁Session,只是浏览器进程结束后,内存中的Cookie(没有设置过期时间的会话Cookie)会被清掉,但服务器端的Session对象还活在内存里,直到超过空闲超时时间,才能被容器回收,所以如果你换个新浏览器,不带旧Cookie,服务器就以为来了个新客。
你可以在Tomcat的web.xml里配置全局会话超时,单位是分钟:
<session-config>
<session-timeout>30</session-timeout>
</session-config>
那个30代表客户端超过30分钟没有动作,容器就会把Session标记为失效。
tomcat会话超时时间怎么调?改一行配置就行
对于大部分中小型项目,直接用Tomcat默认的30分钟就够用,但如果你做的是后台管理系统,希望用户挂机久一点,可以改session-timeout,具体操作路径:
- 找到Tomcat安装目录下的
conf/web.xml。 - 搜
session-config- 修改里面的
session-timeout数值,想改成2小时就写120。- 保存后重启Tomcat,配置才会生效。
- 修改里面的
如果是单个Web应用,不想动全局文件,可以在项目的WEB-INF/web.xml里写同样的配置,优先级高于容器全局配置,顺便说一句,
session-timeout为零或负数时,代表Session永不过期,但这会导致服务器内存只增不减,生产环境千万别这么干。
大型应用里,多个web容器怎么共享一个会话?
单机部署时,容器自己管理Session没毛病,但一旦上了集群,负载均衡会把请求分到不同的Tomcat上,此时如果用户的第一次请求落在节点A,第二次落在节点B,B节点里没有对应的Session,用户就得重新登录,这就是经典的“会话保持”难题。
常见的解法有三个:
- 粘性会话(Sticky Session):负载均衡器根据Session ID或IP,把来自同一客户端的请求固定发给同一个节点,配置简单,但节点挂了,用户的会话也就挂了。
- Session复制:同时部署多个节点时,节点之间互相复制Session数据,实现每个节点都有全量数据,适合节点数少的场景,节点多了会拖垮网络。
- 集中式存储:把Session放到Redis或数据库里,所有节点共享一份数据,这是目前主流做法。
以Redis为例,你只需要在Spring Boot项目里引入spring-session-data-redis,并配置好Redis连接地址,Spring Session框架会接管web容器的Session创建和读取逻辑,把Session数据序列化后存进Redis,这样无论请求落到哪个节点,都能从Redis拿到同一份会话数据,完美解决单点失效问题。
会话安全:你的Session ID可不能被别人看见
Session ID就相当于你家的门钥匙,一旦被第三方抓包截获,对方就能伪装成你访问服务器,这叫Session劫持。
给Cookie加两层“防盗锁”
防止劫持的最直接手段,是让攻击者拿不到Cookie,web容器允许你在配置Cookie时设置两个属性:
- HttpOnly:设置后,浏览器端的JavaScript无法通过
document.cookie读取该Cookie,这样即使网站被注入了XSS脚本,攻击者也偷不走Session ID。 - Secure:这个Cookie只允许在HTTPS连接中传输,如果网站用的是HTTP,浏览器会拒绝发送这个Cookie。
在Tomcat中,你可以通过修改conf/context.xml为默认Servlet注入这两个属性:
<Context useHttpOnly="true" useSecure="true"> ... </Context>
需要注意的是,useSecure会强制整个应用只跑HTTPS,如果你还没部署证书,慎开。
URL重写和隐藏域,是兜底的备胎
在极端情况下,比如客户端禁用了Cookie,web容器会用URL重写方式把Session ID挂在每个链接的后面,类似http://example.com/user/profile;jsessionid=abc123,这种方式的优点是兼容性最强,缺点是Session ID会出现在浏览器地址栏、服务器访问日志、书签里,泄露风险成倍增加。
隐藏域则是表单里藏一个<input type="hidden" name="jsessionid" value="abc123">,只在提交表单时才能传递会话标识,这两种方式都只在Cookie机制完全失效时才值得使用,正常项目尽量保持默认的Cookie方式。
长连接,实时通信里会话怎么办
传统的HTTP会话是“一问一答”,但遇到WebSocket这种长连接,web容器的会话管理逻辑就有点跟不上趟了,WebSocket建立连接后,双方可以随时互推数据,和HTTP会话的超时机制完全两码事。
比如Tomcat 9就支持WebSocket的HttpSession共享,你可以通过HttpSession在WebSocket握手阶段获取会话数据,但一旦WebSocket建立连接,容器就不再维护这个连接的状态了,需要自己设计心跳保活和会话映射,所以做在线聊天、游戏大厅这类功能时,别指望容器管到底,建议把关键状态放到Redis,让容器只管最基础的握手认证。
Q&A:关于web容器会话的最常见疑问
session和cookie的区别是什么?为什么有人说禁用cookie后session就不能用了?
Session本身依赖Cookie来传输Session ID,但也可以改用URL重写,禁用Cookie后,如果应用开发者没有实现URL重写,Session自然就失效了,正规的做法是同时支持两种方式,但出于安全考量,多数框架默认关闭URL重写。
session过期时间是从最后一次请求开始算,还是从创建时间开始算?
从最后一次请求开始算,web容器采用的是“空闲超时”策略只要客户端在设定的时间间隔内没有发起任何请求,这个Session就被视为过期,如果用户一直有操作,超时时间就会不断顺延。
分布式部署时,session共享方案选Redis还是数据库?
绝大多数互联网系统选Redis,因为它是内存数据库,读写速度极快,还天然支持过期时间,数据库存储的优势是持久化强,但每次读写都要走磁盘,会话操作频繁时容易拖垮性能,只有当你的项目规模很小,或者对一致性要求高到必须落盘时,才考虑数据库方案。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/610955.html





