前端鉴权是每一个 Web 应用都绕不开的环节:用户登录之后,前端到底要怎么证明「我是我」,又该如何在每一次请求里安全地携带身份?很多团队把鉴权当成纯后端的事,结果前端随手把 Token 塞进 localStorage、路由层不校验登录态,悄悄埋下 XSS 与越权隐患。本文从 前端鉴权 的三种主流方案(Session-Cookie、JWT、OAuth2/OIDC)讲起,给出可落地的代码示例、选型对照表与上线前检查清单,帮你把身份安全真正落地到浏览器端。
一、为什么前端鉴权值得单独讲
后端负责「签发身份」和「校验身份」,但真正把令牌存哪里、请求怎么带、路由怎么拦,都是前端的主场。一旦前端处理不当,再严密的后端校验也会被绕过:比如把 Access Token 明文存 localStorage 被脚本一锅端,或者页面根本不判断登录态,让未授权用户直接摸到后台入口。所以 前端鉴权 不是可选项,而是整个安全链路里离用户最近的一环。
二、三种主流方案的底层逻辑
2.1 Session-Cookie:服务端记住你是谁
最经典的方案。用户登录后,服务端在内存或 Redis 里保存一份 Session,并把 SessionID 写进 Cookie。之后每次请求浏览器自动带上 Cookie,服务端查表就知道是谁。优点是无状态的前端、注销即时;缺点是服务端要保存状态,跨域和水平扩展时比较麻烦。
2.2 JWT:把状态塞进令牌本身
JWT(JSON Web Token)把「用户身份 + 过期时间 + 签名」编码成一段字符串,服务端不保存会话,只负责签发和验证签名。前端拿到 Token 后,每次请求在 Authorization 头里带上即可。它天然适合前后端分离和跨域,但注销不够即时(令牌在有效期内一直有效,通常需要黑名单或短有效期 + 刷新令牌来补)。
2.3 OAuth2 / OIDC:把登录交给第三方
当你想让用户用微信、GitHub 或公司 SSO 登录时,自己维护账号体系就不划算了。OAuth2 解决「授权」,OIDC(基于 OAuth2)解决「认证」,最终前端拿到的是标准的 ID Token 和 Access Token。它适合第三方登录与单点登录,代价是流程更复杂、要对接授权服务器。
三、一张表看清选型
| 维度 | Session-Cookie | JWT | OAuth2 / OIDC |
| 状态存储位置 | 服务端 | 客户端(令牌内) | 客户端 + 授权服务器 |
| 跨域友好度 | 一般(受 Cookie 限制) | 高 | 高 |
| 注销即时性 | 强(删 Session 即可) | 弱(需黑名单或短时效) | 依赖令牌有效期 |
| 典型场景 | 传统单体 / 后台系统 | 前后端分离 SPA | 第三方登录 / SSO |
四、JWT 实战:签发与前端携带
后端签发时,密钥必须只在服务端,绝不下发到前端。下面是一段 Node 签发示例:
// 服务端签发(Node + jsonwebtoken)
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ uid: 1024, role: 'editor' },
process.env.JWT_SECRET, // 密钥只在服务端,切勿进前端包
{ expiresIn: '2h' }
);
前端拿到 Access Token 后,在每次请求里通过 Authorization 头携带,而不是塞进 URL:
// 前端:每次请求带上 Access Token
fetch('/api/profile', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
五、前端路由守卫:没登录就拦在门外
光有 Token 不够,页面路由也要校验登录态,否则用户手动敲地址就能进后台。以 Vue 3 的 Vue Router 为例,用导航守卫拦住需要登录的页面:
// router/index.js —— 登录态守卫(Vue 3)
router.beforeEach((to) => {
const token = localStorage.getItem('access_token');
if (to.meta.requiresAuth && !token) {
// 未登录则跳转到登录页,并记下来源便于回跳
return { name: 'login', query: { redirect: to.fullPath } };
}
});
六、把 Token 安全送到 WebSocket
实时场景里,WebSocket 连接不像 fetch 那样方便加请求头,常见做法是把 Token 放进连接时的 query 参数或子协议里,由服务端在握手阶段校验:
// 通过 query 传递 token(跨域时不要放 cookie)
const ws = new WebSocket(`wss://api.example.com/ws?token=${accessToken}`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'hello' }));
};
七、四个最容易踩的坑
7.1 把 Token 存 localStorage 被 XSS 偷走
localStorage 任意脚本都能读,一旦页面有 XSS,令牌直接泄露。对安全要求高的场景,优先用 HttpOnly Cookie 存刷新令牌,Access Token 只放内存变量。
7.2 忽略 CSRF 防护
用 Cookie 鉴权时,浏览器会自动附带,攻击者可借用户之手发请求。务必加 CSRF Token 或使用 SameSite=Strict/Lax 的 Cookie 策略。
7.3 不校验过期与签名
前端不能只判断「有没有 Token」,还要看是否过期;真正的校验永远在服务端验签名,前端只做体验层面的提示。
7.4 把密钥写进前端代码
JWT 的签名密钥、OAuth 的 client_secret 都属于服务端机密,打包进前端等于公开。前端最多持有一个公开的 client_id。
八、用反向代理做一层网关防护
鉴权之外,在 Nginx 反向代理 这一层对登录、刷新等敏感接口做限流,能有效挡住爆破和刷接口:
# 限制登录接口被暴力破解
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
server {
location /api/login {
limit_req zone=login burst=10 nodelay;
proxy_pass http://auth-backend;
}
}
九、上线前检查清单
| 检查项 | 是否达标 |
| Access Token 是否短时效(建议 15min~2h) | □ |
| 刷新令牌是否 HttpOnly + Secure + SameSite | □ |
| 路由守卫是否覆盖所有 requiresAuth 页面 | □ |
| 敏感接口是否开启限流(见上文 Nginx) | □ |
| 登录失败是否有防爆破(锁账号 / 验证码) | □ |
| 是否接入 GitHub Actions 做依赖与密钥扫描 | □ |
十、刷新令牌怎么安全设计
短时效的 Access Token 虽然安全,但用户每过一两个小时就被强制重新登录,体验会非常糟糕。解决办法是引入刷新令牌(Refresh Token):Access Token 只活 15 分钟左右,过期后前端用 Refresh Token 静默换发新的 Access Token,用户无感知。Refresh Token 本身有效期更长,但必须存放在 HttpOnly Cookie 里,并且支持轮换——每次使用都作废旧令牌、签发新令牌,这样即便被盗也能被快速识别并吊销。
// 前端:Access Token 过期后用 Refresh Token 换新
// Refresh Token 走 HttpOnly Cookie,前端不直接持有,靠 credentials 自动带
async function refreshAccessToken() {
const res = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include'
});
const { access_token } = await res.json();
localStorage.setItem('access_token', access_token);
}
| 对比项 | Access Token | Refresh Token |
| 有效期 | 短(15min~2h) | 长(7~30 天) |
| 存储位置 | 前端内存变量 | HttpOnly Cookie |
| 主要用途 | 业务接口鉴权 | 换发 Access Token |
| 泄露影响 | 短期、受限 | 需配合轮换与吊销列表 |
十一、小结
前端鉴权没有银弹:单体后台用 Session-Cookie 最省心,前后端分离用 JWT 最顺手,要接第三方登录就上 OAuth2/OIDC。真正决定安全水平的,是把令牌存对地方、把路由守严、把密钥留在服务端这些细节。把本文的代码示例和检查清单带进你的下一个项目,就能少踩一大半坑。开发联调时,配合 Docker 一键拉起鉴权服务和 VS Code 的 REST 调试插件,能大幅加快验证节奏。



