Authentication实战中常见的安全漏洞与防御策略
谁更灵活?谁更省事?
这问题我去年在三个项目里反复问自己:登录态怎么管?不是“要不要鉴权”,而是“用啥管最不扯皮”。JWT、Session、OAuth 2.0(含 PKCE)、还有现在越来越火的 Cookie + HttpOnly + SameSite 组合——真上手写一遍,才发现文档写得再漂亮,一进生产环境全是坑。
我先说结论:中小型后台系统,我选 Cookie + HttpOnly + SameSite + CSRF Token;中大型 SaaS 或需要第三方登录的,直接上 OAuth 2.1 + PKCE(别再写 OAuth 2.0 了,RFC 6749 已过时);JWT?除非你真有无状态网关或跨域极简架构,否则别碰它做主认证方案——我踩过三次坑,最后一次是凌晨三点改完发现 token 过期后刷新逻辑崩了,用户卡在白屏上刷不出首页。
Session vs JWT:不是技术之争,是运维认知差
先说 Session。很多人一听“服务端存 session”就皱眉,觉得“不 scalable”。但现实是:Redis Cluster 我司用了五年,单集群扛 30w+ 并发会话,没出过一次 session 丢失。而 JWT 呢?你真敢把用户角色、权限列表全塞进 payload?那 token 动不动 2KB+,HTTP Header 撑爆、CDN 缓存失效、移动端弱网下重试率飙升。我们有个客户 App 就因为 JWT 太大,iOS WKWebView 在某些 iOS 15.4 下直接拒绝携带 Authorization header——折腾两天才定位到是 base64url 编码后的 token 长度超限。
Session 的代码其实超级干净:
// Express + Redis-Store 示例(精简版)
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redisClient = require('redis').createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-here',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // 生产必须开
sameSite: 'lax', // 注意:lax 对 POST 表单友好,strict 会断掉部分跳转
maxAge: 24 * 60 * 60 * 1000 // 24小时
}
}));
// 登录成功后
req.session.userId = user.id;
req.session.role = user.role;
req.session.save(); // 显式保存,避免异步问题
关键点:sameSite: 'lax' 是我目前最稳的选择;maxAge 要和前端 document.cookie 的过期逻辑对齐;千万别忘了 req.session.save()——我第一次上线漏了这句,用户登完即失,查日志查到怀疑人生。
JWT:看着自由,实则枷锁
JWT 我只在两种场景用:一是内部微服务间调用(用 RSA 公私钥签名校验),二是临时凭证(比如密码重置链接里的 exp=300)。做主认证?太重了。
一个典型反模式:
// ❌ 错误示范:把所有权限塞进 JWT
const payload = {
userId: 123,
email: 'user@jztheme.com',
roles: ['admin', 'editor', 'report_viewer'],
permissions: ['user:read', 'post:write', 'dashboard:export'],
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600
};
const token = jwt.sign(payload, process.env.JWT_SECRET);
问题在哪?用户被移除 admin 角色,token 还有效一小时;你没法主动作废;前端 localStorage 存 token,XSS 一扫全丢;你还得自己实现 refreshToken 逻辑,又绕回“服务端要维护 refresh token 黑名单”——那跟 Session 有啥区别?只是把 Redis 换成数据库表而已。
所以我的规则很粗暴:JWT 只用于 stateless 场景,且 payload 必须极简(仅 userId + exp),所有权限校验一律走服务端接口(比如 /api/v1/me/permissions)。
OAuth 2.1 + PKCE:第三方登录的事实标准
如果你的产品要接入微信、GitHub、Google,或者未来要开放给客户自建 IDP,OAuth 2.1(RFC 9126)是唯一靠谱选项。PKCE 不是可选项,是必选项——去年我们一个客户因没加 PKCE,被安全审计打回重做。
前端(React + @react-oauth/google)就这么几行:
import { GoogleOAuthProvider, useGoogleLogin } from '@react-oauth/google';
function LoginButton() {
const login = useGoogleLogin({
flow: 'auth-code',
scope: 'openid profile email https://www.googleapis.com/auth/userinfo.email',
onSuccess: async (codeResponse) => {
// 拿 code 换 token,这步必须后端做!
const res = await fetch('https://jztheme.com/api/auth/google/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: codeResponse.code })
});
const { accessToken } = await res.json();
localStorage.setItem('auth_token', accessToken);
window.location.href = '/dashboard';
}
});
return <button onClick={() => login()}>用 Google 登录</button>;
}
重点:code 换 token 的步骤必须由后端完成(防止泄露 client_secret),前端只拿最终 access_token。这个 token 我一般设为短期(15min),配合后端的 refresh 流程。比自己搞 JWT 刷新简单太多——OAuth provider 已经帮你把所有边界 case 写死了。
Cookie + SameSite + CSRF:被低估的王者组合
这是我目前主力推荐的方案,尤其适合管理后台、ERP、CRM 这类不需要开放第三方登录的系统。它不炫技,但稳得一批。
后端(Express)配好 Cookie 后,前端登录请求只要这样:
// 登录请求,不用手动带 cookie,浏览器自动带
fetch('https://jztheme.com/api/login', {
method: 'POST',
credentials: 'include', // 关键!
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
.then(res => res.json())
.then(data => {
if (data.success) window.location.href = '/dashboard';
});
CSRF Token 怎么防?很简单:首次 GET /login 时后端返回一个隐藏字段,表单提交带上它;或者用 SameSite=Lax + POST 表单天然防御大部分 CSRF(Lax 模式下跨站 POST 不带 Cookie)。我们线上跑了一年,零 CSRF 攻击记录。
缺点?就是不能用在纯静态站点(比如 GitHub Pages 托管的登录页),因为没法服务端 set-cookie。但如果你项目是 Next.js、Nuxt 或常规 SSR,这组合真的香。
我的选型逻辑
- 内部系统、快速上线、团队后端强于前端 → Cookie + SameSite + CSRF Token(最快落地,最易 audit)
- 要接微信/钉钉/飞书/Google 等第三方 → OAuth 2.1 + PKCE(别省那点开发量,安全是底线)
- 微服务网关层统一鉴权、无状态要求高 → JWT(但 payload 只放 userId,权限全走后端 check)
- JWT?除非你能保证:1)永远不 revoke token;2)前端绝对防住 XSS;3)token size < 1KB;4)团队有专人维护密钥轮换流程 —— 否则免谈。
最后说一句大实话:没有银弹。上周我还在一个老项目里硬着头皮用 Session + Memcached(客户不让上 Redis),照样跑得飞起。工具是死的,人是活的。比起纠结“哪个技术更高级”,不如多花十分钟写个清晰的错误提示——比如“登录失败,请检查网络或联系管理员”,而不是“Auth Error: 401 Invalid signature”,后者只会让客服电话被打爆。
以上是我踩坑后的总结,希望对你有帮助。有不同看法欢迎评论区交流,尤其是 OAuth 2.1 实战中遇到的坑,我很想听听你们是怎么填的。

暂无评论