2025-11-23-关于 JSON Web Token(JWT)
前端身份验证管理-基于 JWT Token 的实现
0x01 JWT 简介与工作原理
JSON Web Token(JWT)是 RFC 7519 定义的一种开放标准,用来在网络应用之间安全地传递声明。它最大的特点是紧凑且自包含:一个令牌就携带了验证身份所需的全部信息,不需要服务端再去查库或维护会话。整个令牌由三段组成,分别是 Header、Payload 和 Signature,三者各自经过 Base64Url 编码后用点号连接起来,最终形成一串形如 xxxxx.yyyyy.zzzzz 的字符串。
Header 描述的是令牌本身的元信息,通常只包含类型 "typ": "JWT" 和签名算法 "alg": "HS256" 或 "RS256"。Payload 是真正承载业务含义的部分,里面既有标准的注册声明,比如签发者 iss、过期时间 exp、主题 sub,也可以放自定义声明,例如用户 ID 或角色。这里需要特别强调一点:Payload 只是被编码,并没有被加密,任何人拿到令牌都能解出里面的内容,所以绝不能在其中存放密码、密钥这类敏感数据。第三段 Signature 才是安全的关键,它用密钥对前两段做签名,一旦有人篡改了 Header 或 Payload,签名校验就会失败,从而保证令牌的完整性和真实性。
一次典型的 JWT 认证流程是这样展开的:用户先用账号密码发起登录,服务器核对凭证无误后,签发一个 JWT 返回给客户端;客户端把它存下来,可以放在 localStorage,也可以放在 HttpOnly Cookie 里;之后每次请求,客户端都在 Authorization 头里以 Bearer <token> 的形式带上这个令牌;服务器收到后校验签名、过期时间和各项声明,确认无误就放行访问。和传统的 Session 认证相比,JWT 的好处在于无状态——服务端不需要保存任何会话信息,天然适合分布式系统横向扩展。但无状态也带来了一个代价:令牌一旦签发就无法主动撤销,只能依靠较短的过期时间和刷新机制来缓解风险。
0x02 前端 Token 存储与管理
前端管理 JWT 的核心思路,是把”设置认证态”和”清除认证态”这两个动作收敛成统一的入口,通常借助一个封装好的 Axios 实例来实现,对应 setAuthToken 和 clearAuthToken 两个函数。
存储方式的选择直接关系到安全性。最稳妥的做法是把令牌放进带有 HttpOnly、Secure 和 SameSite 属性的 Cookie 里:HttpOnly 让 JavaScript 无法读取它,从根上挡住了 XSS 窃取;Secure 保证它只在 HTTPS 下传输;SameSite 则能在很大程度上缓解 CSRF。相比之下,localStorage 和 sessionStorage 虽然用起来方便,但内容对脚本完全可见,一旦页面存在 XSS 漏洞,令牌就会被轻易盗走。如果业务上确实绕不开用 JavaScript 来存储,那至少要把它和较短的过期时间、刷新令牌机制结合起来,缩小被盗后的危害窗口。
在工程实现上,可以把 Axios 实例单独抽成一个 apiClient.ts 文件,所有请求都从这个实例发出,认证头的增删也集中在这里管理:
import axios from 'axios';
const apiClient = axios.create({ baseURL: '/api' });
export const setAuthToken = (token: string) => { if (token) { apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`; }};
export const clearAuthToken = () => { delete apiClient.defaults.headers.common['Authorization'];};
// 请求拦截器:可选统一添加其他逻辑apiClient.interceptors.request.use(config => { // 可从 Cookie 或存储中读取 token return config;});
export default apiClient;这样一来,登录成功后只要把拿到的令牌存下来并调用 setAuthToken,后续所有请求就会自动带上认证头:
// 登录接口调用后const response = await login(username, password);localStorage.setItem('token', response.data.token); // 或依赖 CookiesetAuthToken(response.data.token);登出时则反过来,把内存里的认证头和持久化的令牌一并清掉:
clearAuthToken();localStorage.removeItem('token'); // 或清除 Cookie0x03 通过钩子或守卫验证用户权限
光有令牌还不够,还得在路由层面拦住未认证的访问。各个前端框架都提供了相应的机制——Vue 里是路由守卫,React 里则常用自定义钩子或包装组件——它们的共同目标是:在用户真正进入受保护页面之前,先检查一遍登录状态。
在 Vue 中,可以在全局前置守卫 beforeEach 里判断目标路由是否需要认证。通过给路由配置 meta.requiresAuth 标记,再结合从存储中读取的令牌,就能决定是放行还是重定向到登录页:
import router from '@/router';import { getToken } from '@/utils/auth'; // 从存储获取 token
router.beforeEach((to, from, next) => { if (to.meta.requiresAuth) { // 路由元信息标记需认证 const token = getToken(); if (token) { // 可选:调用后端验证 token 有效性 next(); } else { next('/login'); // 重定向登录 } } else { next(); }});React 的思路类似,只是落地形式换成了一个包装组件。下面这个 ProtectedRoute 会在渲染子组件前先检查令牌是否存在,没有就直接跳转登录页。实际项目中往往还会配合 Context 或 Redux 来集中管理认证状态,而不是每次都直接读 localStorage:
import { Navigate } from "react-router-dom";
const ProtectedRoute = ({ children }) => { const token = localStorage.getItem("token"); return token ? children : <Navigate to="/login" />;};除了页面级的拦截,还可以更进一步利用 Payload 里的角色声明做细粒度的权限控制。比如根据用户角色决定某个操作按钮是否渲染,从而实现按钮级别的隐藏,让权限管理一直延伸到界面的每一个交互元素。
0x04 Token 刷新与过期处理
短过期时间能提升安全性,却会牺牲体验——总不能让用户每隔一小时就重新登录一次。刷新令牌机制正是为了在两者之间取得平衡。它的做法是把令牌拆成两个:一个是短期有效的 Access Token,通常 15 到 60 分钟就过期,用于日常请求;另一个是长期有效的 Refresh Token,安全地存放在 HttpOnly Cookie 中,专门用来换取新的 Access Token。
具体到前端,关键在于响应拦截器。当某个请求因为 Access Token 过期而收到 401 时,拦截器不直接把错误抛给业务代码,而是先悄悄调用刷新接口拿到新令牌,更新认证头,再把刚才失败的那个请求原样重发一遍。整个过程对上层调用者是透明的,用户完全感知不到中间发生过一次令牌续期:
apiClient.interceptors.response.use( response => response, async error => { if (error.response.status === 401) { // 调用刷新接口,更新 token const newToken = await refreshToken(); setAuthToken(newToken); // 重试原请求 return apiClient(error.config); } return Promise.reject(error); });需要补充的是,真实场景里还要考虑并发请求同时触发 401 的情况——如果多个请求几乎同时过期,应该用一个”刷新中”的标志位或队列把它们挂起,等单次刷新完成后再统一重放,避免对刷新接口发起多次重复调用。
0x05 前后端交互鉴权
前面几节大多站在前端视角讨论令牌的存取和拦截,但鉴权本质上是一次前后端的协作。要真正理解 JWT,必须把后端这一半补齐,看清一个令牌从签发、携带到校验的完整闭环。
后端的职责从登录那一刻就开始了。用户提交凭证后,服务器先核对账号密码——通常是把数据库里存的密码哈希和提交值做比对,绝不会明文存储或比较——确认无误后,就用只有自己掌握的密钥对一组声明做签名,生成 Access Token 和 Refresh Token 返回。以 FastAPI 为例,这部分逻辑一般集中在 core/security.py 里,由它统一负责密码哈希的校验、令牌的生成以及后续的解码验证,把所有和安全相关的细节锁在一个模块内:
from datetime import datetime, timedeltafrom jose import jwtfrom passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed)
def create_access_token(subject: str, expires_minutes: int = 30) -> str: expire = datetime.utcnow() + timedelta(minutes=expires_minutes) payload = {"sub": subject, "exp": expire, "iss": "my-app"} return jwt.encode(payload, SECRET_KEY, algorithm="HS256")令牌发出去之后,前端就按 0x02 里说的方式,在 Authorization 头中以 Bearer <token> 的形式带回来。后端这边接住它的入口,在 FastAPI 中是依赖注入。框架提供的 OAuth2PasswordBearer 会自动从请求头里把令牌抽出来,再交给一个依赖函数去解码和验证。这种写法的妙处在于,业务接口本身不用关心鉴权细节,只要在函数签名里声明一个 Depends(get_current_user),所有未携带令牌或令牌无效的请求都会在进入业务逻辑之前就被拦下:
from fastapi import Depends, HTTPExceptionfrom fastapi.security import OAuth2PasswordBearerfrom jose import jwt, JWTError
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def get_current_user(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) user_id = payload.get("sub") if user_id is None: raise HTTPException(status_code=401, detail="无效令牌") except JWTError: raise HTTPException(status_code=401, detail="令牌校验失败") return get_user(user_id)
@app.get("/profile")def read_profile(user = Depends(get_current_user)): return user校验环节真正做的事,远不止”解出 Payload”这么简单。后端必须用签发时的密钥重新算一遍签名,确认令牌没被篡改;然后检查 exp 判断是否过期,检查 iss 确认确实是自己签发的,必要时还要校验 aud 确保令牌是发给当前这个服务用的。这几道关卡全部通过,才能信任 Payload 里的用户身份。可以看到,前端的 Bearer 头和后端的签名校验是严丝合缝的一对:前端负责安全地保管和携带,后端负责签发和验证,密钥始终只留在后端,令牌在网络上即便被看到,没有密钥也无法伪造。
WebSocket 这类长连接的鉴权稍微特殊一些,因为它建立连接时不像普通 HTTP 请求那样方便逐次带头。常见做法是在握手阶段把令牌作为参数传过去,由服务端在连接建立的回调里完成一次性校验。比如协作类服务常用的 onAuthenticate 钩子,就是在客户端尝试接入时拿到令牌、解码验证,通过了才允许建立连接,之后这条通道上的所有消息都默认已认证,不必再逐条校验。
0x06 安全实践
把上面的机制落到生产环境,还有一系列工程上的细节需要守住。最基础的一条是全程使用 HTTPS,否则令牌在传输途中就可能被截获,前面所有的签名校验都失去意义。服务端的校验必须严格,签名、exp、iss、aud 一个都不能省,任何一项不过关都应当拒绝请求。Payload 里坚决不放敏感信息,因为它本质上是公开可读的。针对 CSRF,要靠 SameSite Cookie 或额外的 token 来防护。密钥需要定期轮换,并支持刷新令牌的旋转,让长期凭证也能周期性失效。最后,对异常登录行为做监控,在短期令牌的前提下,必要时还可以引入黑名单作为补充手段,弥补 JWT 无法主动撤销的固有短板。
0x07 参考资料
- RFC 7519: JSON Web Token
- OWASP JWT Cheat Sheet
- Auth0 JWT Best Practices
- Curity JWT Security Guide
分享到社交平台
将本文分享给你的朋友们
Zhongye