4. 会话管理:会话令牌安全、超时策略、并发会话控制
聊到会话管理,我脑子里第一个蹦出来的画面,是几年前帮一家制造企业做安全审计。他们的ERP系统,用户登录后,只要不关浏览器,哪怕隔了一个周末回来,页面还是活的。你想想看,这有多危险?
会话管理,说白了就是系统怎么认你、怎么记住你、又怎么在你离开后把你忘掉。这里面有三个核心问题:令牌怎么藏、超时怎么设、并发怎么管。一个一个说。
4.1 会话令牌安全:别让坏人捡到你的“房卡”
会话令牌就像酒店的房卡。你办入住时前台给你一张,你拿着它能开门、能去健身房。但要是这张卡丢了,别人捡到就能进你房间。
我见过最离谱的做法,是把令牌直接放在URL里。比如:http://erp.company.com/dashboard?token=abc123。这等于把房卡贴在门上,谁路过都能看见。
正确的做法是什么?
- 令牌必须放在HTTP Only的Cookie里。这样JavaScript拿不到,XSS攻击也偷不走。
- 加上Secure标志。只允许HTTPS传输,防止中间人截获。
- 设置SameSite属性。我习惯设成
Strict或Lax,防止CSRF攻击。
核心配置示例(Java Spring Security):
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(new CustomSessionFilter(), UsernamePasswordAuthenticationFilter.class);
令牌本身也要够复杂。别用自增ID,别用时间戳。我建议用UUID或者随机生成的64位字符串。长度至少32字节,别偷懒。
⚠️ 我曾经踩过的坑: 有次我发现令牌生成用的是new Random().nextLong()。Random类不够随机,理论上可以被预测。后来全部换成了SecureRandom。嗯,这个细节很多人会忽略。
4.2 超时策略:该放手时就放手
超时策略分两种:空闲超时和绝对超时。
空闲超时,就是用户不动了,多久踢出去。我个人习惯,ERP系统里设15到30分钟。太短了用户烦,太长了不安全。
绝对超时,不管用户动不动,到点就失效。一般设8小时,跟一个工作日差不多。
| 场景 | 空闲超时 | 绝对超时 | 说明 |
|---|---|---|---|
| 财务模块 | 10分钟 | 4小时 | 敏感操作,短点好 |
| 采购审批 | 20分钟 | 8小时 | 审批流程可能较长 |
| 报表查看 | 30分钟 | 12小时 | 只读操作,可以宽松些 |
超时到了怎么办?别直接丢个空白页。我建议弹个友好的提示:“您的会话已超时,请重新登录。” 然后把用户当前页面URL记下来,登录后跳回去。用户体验会好很多。
💡 一个小技巧: 在超时前1分钟,可以发个心跳请求给前端。前端收到后弹个倒计时:“您还有60秒空闲,是否继续?” 用户点一下,超时计时器重置。这比突然踢人温和多了。
4.3 并发会话控制:一个人开几扇门?
并发会话,就是一个账号同时在多个设备上登录。ERP系统里,这通常是个大问题。
我记得有家客户,销售团队共用几个账号。一个人登录了,另一个人再登录,前面那个就被踢了。结果销售们怨声载道,说系统不好用。其实不是系统不好用,是并发策略没设对。
常见的并发策略有三种:
- 允许无限并发:最宽松,但最不安全。适合只读的报表账号。
- 踢掉旧会话:新登录时,旧会话失效。适合普通员工账号。
- 拒绝新登录:旧会话还在时,新登录被拒绝。适合管理员账号。
我一般这样配置:
# 在application.yml中配置
server:
servlet:
session:
timeout: 30m
tracking-modes: cookie
session:
concurrent:
max-sessions: 1
max-sessions-prevent-login: false # false表示踢旧,true表示拒新
这里有个细节:max-sessions-prevent-login设为false,就是踢旧;设为true,就是拒新。我建议普通用户用踢旧,管理员用拒新。为什么?管理员账号一旦被盗,坏人登录了,好人就被踢了,好人能立刻发现问题。要是拒新,坏人登录不上,但好人也不知道自己账号已经被试过了。
⚠️ 注意: 并发控制一定要跟用户通知配合。不管是踢旧还是拒新,都要告诉用户发生了什么。比如踢旧时,被踢的设备上弹个提示:“您的账号在其他设备登录,您已被登出。” 这样用户才知道怎么回事。
4.4 会话固定攻击:别让坏人“固定”你的身份
会话固定攻击,就是攻击者先拿一个合法的会话ID,然后骗你用这个ID登录。你一登录,攻击者就能用同一个ID冒充你。
怎么防?很简单:登录成功后,重新生成会话ID。
我习惯在登录认证成功后,调用request.changeSessionId()。这样旧的会话ID就废了,攻击者手里的那个ID也就没用了。
// 登录成功后
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 重新生成会话ID,防止会话固定攻击
request.changeSessionId();
// 其他逻辑...
}
嗯,这个操作虽然简单,但很多框架默认不开启。你得手动配一下。
4.5 会话数据的保护
会话里存什么?我见过有人把用户密码明文存进去的。这简直是给自己埋雷。
会话里应该只存:
- 用户ID(不要用用户名,用内部ID)
- 角色和权限列表(尽量精简)
- 一些临时状态(比如当前操作的单据号)
绝对不要存:
- 密码、密钥等敏感信息
- 完整的用户对象(里面可能包含敏感字段)
- 数据库连接信息
另外,会话数据最好加密存储。尤其是分布式部署时,会话可能存到Redis里。Redis如果不加密,别人连上就能看到所有会话内容。我建议用AES-256加密,密钥单独管理。
总结一下会话管理的几个关键点:
- 令牌放Cookie里,加HttpOnly、Secure、SameSite
- 超时策略分空闲和绝对,按模块差异化配置
- 并发控制选踢旧还是拒新,配合用户通知
- 登录后重新生成会话ID,防会话固定攻击
- 会话数据精简,敏感信息别往里塞
会话管理这块,说白了就是平衡安全性和易用性。太严了用户骂娘,太松了安全部门找你喝茶。我这些年摸索出来的经验是:先按最严格的来,然后根据用户反馈逐步放宽。毕竟,安全可以加,但出了事再想收回来,就难了。