后端校验(在jwt中,token的过期时间如何控制)

后端校验(在jwt中,token的过期时间如何控制)
在jwt中,token的过期时间如何控制

在 JWT 中,Token 过期时间的控制是保障安全性的核心环节,核心逻辑是在 Token 的 Payload 中嵌入 exp 字段(过期时间戳),服务器验证时自动校验该字段。下面从「基础控制方式」「进阶优化方案」「实战代码」三个维度,讲清楚如何灵活、安全地控制 Token 过期时间,覆盖 Tornado 后端 + Vue/TS 前端的完整实现。


一、核心概念:JWT 过期时间的基础规则

1. 核心字段:exp(Expiration Time)

  • exp 是 JWT 标准字段,值为Unix 时间戳(秒)(注意:不是毫秒!);
  • 服务器验证 Token 时,会自动对比当前时间戳和 exp,若当前时间 > exp,则判定 Token 过期;
  • exp 必须嵌入 Payload,且由服务器生成(前端无法篡改,因为签名会验证 Payload 完整性)。

2. 其他可选时间字段(辅助控制)

表格

字段

含义

作用

nbf

Not Before

Token 生效时间(在此时间前无法使用)

iat

后端校验(在jwt中,token的过期时间如何控制)

Issued At

Token 签发时间(可用于校验 Token 签发时长)

jti

JWT ID

Token 唯一标识(用于黑名单、刷新控制)


二、Token 过期时间的 3 种控制方式(按场景选择)

方式 1:固定过期时间(基础版)

核心逻辑

登录时生成 Token,直接在 Payload 中设置固定的 exp(如 2 小时),适用于简单场景。

实战代码(Tornado 后端)

python

运行

import jwtimport timefrom typing import Dict# JWT 配置JWT_SECRET_KEY = "your_secure_key"  # 生产环境放环境变量JWT_ALGORITHM = "HS256"JWT_EXPIRE_SECONDS = 7200  # 2小时过期(秒)def generate_token(user_info: Dict) -> str:    """生成带固定过期时间的 Token"""    payload = {        "user_id": user_info["id"],        "username": user_info["username"],        "iat": int(time.time()),  # 签发时间(可选)        "exp": int(time.time()) + JWT_EXPIRE_SECONDS  # 核心:过期时间戳    }    # 生成 Token    token = jwt.encode(payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)    return token# 验证 Token 时,自动校验 expdef verify_token(token: str) -> Dict | None:    try:        # jwt.decode 会自动校验 exp,无需手动判断        payload = jwt.decode(            token,            JWT_SECRET_KEY,            algorithms=[JWT_ALGORITHM],            options={"verify_exp": True}  # 显式开启 exp 校验(默认开启)        )        return payload    except jwt.ExpiredSignatureError:        print("Token 已过期")        return None    except jwt.InvalidTokenError:        print("Token 无效")        return None

方式 2:滑动过期(用户活跃时自动续期)

核心问题

固定过期时间的缺陷:用户正在操作时 Token 过期,体验差(比如 2 小时到期,用户第 1 小时 59 分操作,仍会过期)。

核心逻辑

  • 每次用户发起有效请求时,检查 Token 剩余有效期;
  • 若剩余时间 < 阈值(如 30 分钟),自动生成新 Token 并返回给前端;
  • 前端替换旧 Token,实现 “活跃即续期”。

实战代码(Tornado 后端 + Vue/TS 前端)

后端(Tornado):滑动过期逻辑

python

运行

def refresh_token_if_needed(token: str) -> str | None:    """检查 Token 剩余时间,不足则刷新"""    try:        payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM], options={"verify_exp": True})        current_time = int(time.time())        remaining_seconds = payload["exp"] - current_time                # 阈值:剩余时间 < 30 分钟(1800 秒)则刷新        if remaining_seconds < 1800:            # 生成新 Token(过期时间重置为 2 小时)            new_payload = {                "user_id": payload["user_id"],                "username": payload["username"],                "iat": current_time,                "exp": current_time + JWT_EXPIRE_SECONDS            }            new_token = jwt.encode(new_payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)            return new_token        return None  # 无需刷新    except Exception:        return None# 在受保护的接口中使用class ProfileHandler(BaseHandler):    @jwt_auth_required    async def get(self):        # 1. 获取旧 Token        auth_header = self.request.headers.get("Authorization", "")        old_token = auth_header.split(" ")[1] if auth_header else ""                # 2. 检查是否需要刷新 Token        new_token = refresh_token_if_needed(old_token)                # 3. 返回用户信息 + 新 Token(如有)        response_data = {            "code": 0,            "data": {"user_id": self.current_user["user_id"], "username": self.current_user["username"]}        }        if new_token:            response_data["data"]["new_token"] = new_token  # 携带新 Token                self.write(response_data)

前端(Vue + TS):自动更新 Token

typescript

运行

// src/utils/request.ts(修改响应拦截器)request.interceptors.response.use(  (response) => {    const data = response.data;    // 若返回新 Token,自动替换旧 Token    if (data.data?.new_token) {      setToken(data.data.new_token); // 调用之前封装的 setToken 方法    }    return data;  },  (error) => {    // 原有 401 处理逻辑...    return Promise.reject(error);  });

方式 3:双 Token 机制(进阶版,生产环境推荐)

核心问题

滑动过期仍有缺陷:单一 Token 过期后用户需重新登录,体验差;且 Token 有效期过长(如 7 天)不安全,过短(如 15 分钟)频繁刷新影响性能。

核心逻辑

生成两个 Token:

  • Access Token:短期有效(如 2 小时),用于接口访问(核心 Token);
  • Refresh Token:长期有效(如 7 天),仅用于刷新 Access Token(不参与业务接口验证)。

完整流程

预览

查看代码

后端前端后端(Tornado)前端(Vue+TS)后端前端后端(Tornado)前端(Vue+TS)登录请求返回 access_token(2h) + refresh_token(7d)存储双 Token(refresh_token 存在 HttpOnly Cookie)携带 access_token 调用接口返回业务数据携带 access_token 调用接口返回 401(access_token 过期)携带 refresh_token 调用 /refresh_token返回新的 access_token(2h)携带新 access_token 调用接口返回业务数据

sequenceDiagram    participant 前端(Vue+TS)    participant 后端(Tornado)    # 1. 登录生成双 Token    前端->>后端: 登录请求    后端->>前端: 返回 access_token(2h) + refresh_token(7d)    前端->>前端: 存储双 Token(refresh_token 存在 HttpOnly Cookie)        # 2. 正常请求    前端->>后端: 携带 access_token 调用接口    后端->>前端: 返回业务数据        # 3. access_token 过期    前端->>后端: 携带 access_token 调用接口    后端->>前端: 返回 401(access_token 过期)        # 4. 刷新 Token    前端->>后端: 携带 refresh_token 调用 /refresh_token    后端->>前端: 返回新的 access_token(2h)        # 5. 继续请求    前端->>后端: 携带新 access_token 调用接口    后端->>前端: 返回业务数据

后端前端后端(Tornado)前端(Vue+TS)后端前端后端(Tornado)前端(Vue+TS)登录请求返回 access_token(2h) + refresh_token(7d)存储双 Token(refresh_token 存在 HttpOnly Cookie)携带 access_token 调用接口返回业务数据携带 access_token 调用接口返回 401(access_token 过期)携带 refresh_token 调用 /refresh_token返回新的 access_token(2h)携带新 access_token 调用接口返回业务数据



实战代码

后端(Tornado):生成 / 刷新双 Token

python

运行

# 双 Token 配置ACCESS_TOKEN_EXPIRE = 7200  # 2小时REFRESH_TOKEN_EXPIRE = 604800  # 7天def generate_double_tokens(user_info: Dict) -> Dict:    """生成 access_token + refresh_token"""    current_time = int(time.time())    # 1. 生成 access_token    access_payload = {        "user_id": user_info["id"],        "username": user_info["username"],        "type": "access",  # 标记 Token 类型        "exp": current_time + ACCESS_TOKEN_EXPIRE    }    access_token = jwt.encode(access_payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)        # 2. 生成 refresh_token(额外存储到 Redis,便于主动失效)    refresh_payload = {        "user_id": user_info["id"],        "type": "refresh",        "exp": current_time + REFRESH_TOKEN_EXPIRE,        "jti": str(uuid.uuid4())  # 唯一标识,用于 Redis 存储    }    refresh_token = jwt.encode(refresh_payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)        # 3. refresh_token 存储到 Redis(便于登出/过期清理)    redis_client.setex(        f"refresh_token:{refresh_payload['jti']}",        REFRESH_TOKEN_EXPIRE,        user_info["id"]    )        return {        "access_token": access_token,        "refresh_token": refresh_token    }# 刷新 Token 接口class RefreshTokenHandler(BaseHandler):    async def post(self):        # 1. 获取 refresh_token(优先从 HttpOnly Cookie 获取)        refresh_token = self.get_cookie("refresh_token", "") or self.get_body_argument("refresh_token", "")        if not refresh_token:            self.set_status(401)            self.write({"code": -1, "msg": "缺少 refresh_token"})            return                # 2. 验证 refresh_token        try:            payload = jwt.decode(refresh_token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])            # 校验 Token 类型和 Redis 中是否存在            if payload.get("type") != "refresh":                raise jwt.InvalidTokenError("Token 类型错误")            if not redis_client.get(f"refresh_token:{payload['jti']}"):                raise jwt.ExpiredSignatureError("refresh_token 已失效")                        # 3. 生成新的 access_token            user_info = {"id": payload["user_id"], "username": payload["username"]}            new_access_token = generate_token(user_info)  # 复用之前的 generate_token 方法                        self.write({                "code": 0,                "data": {"access_token": new_access_token}            })        except jwt.ExpiredSignatureError:            self.set_status(401)            self.write({"code": -1, "msg": "refresh_token 已过期,请重新登录"})        except Exception:            self.set_status(401)            self.write({"code": -1, "msg": "refresh_token 无效"})# 登录接口返回双 Tokenclass LoginHandler(BaseHandler):    async def post(self):        # 验证用户名密码(省略)        user_info = {"id": 1001, "username": "admin"}        tokens = generate_double_tokens(user_info)                # refresh_token 存入 HttpOnly Cookie(更安全)        self.set_cookie(            "refresh_token",            tokens["refresh_token"],            httpOnly=True,            secure=True,  # 生产环境开启            expires=time.time() + REFRESH_TOKEN_EXPIRE,            path="/"        )                # 返回 access_token(refresh_token 不返回前端,仅存在 Cookie)        self.write({            "code": 0,            "data": {"access_token": tokens["access_token"]}        })

前端(Vue + TS):刷新 Token 逻辑

typescript

运行

// src/utils/request.ts(优化 401 处理)import axios from 'axios';import { getToken, setToken, removeToken } from './jwt';import { ElMessage } from 'element-plus';const request = axios.create({  baseURL: 'http://localhost:8888/api',  timeout: 5000});// 创建刷新 Token 的请求实例(避免拦截器循环)const refreshRequest = axios.create({  baseURL: 'http://localhost:8888/api',  timeout: 5000,  withCredentials: true  // 携带存储 refresh_token 的 Cookie});// 用于防止重复刷新 Tokenlet isRefreshing = false;// 存储等待刷新 Token 的请求队列let requestQueue: Function[] = [];// 请求拦截器(携带 access_token)request.interceptors.request.use(  (config) => {    const token = getToken();    if (token) {      config.headers.Authorization = `Bearer ${token}`;    }    return config;  },  (error) => Promise.reject(error));// 响应拦截器(处理 401 刷新 Token)request.interceptors.response.use(  (response) => response.data,  async (error) => {    const originalRequest = error.config;    // 1. 排除刷新 Token 接口本身的 401    if (error.response?.status === 401 && originalRequest.url !== '/refresh_token' && !originalRequest._retry) {      originalRequest._retry = true; // 标记已重试,防止循环            if (!isRefreshing) {        isRefreshing = true;        try {          // 2. 调用刷新 Token 接口          const res = await refreshRequest.post('/refresh_token');          if (res.code === 0) {            // 3. 存储新的 access_token            setToken(res.data.access_token);            // 4. 重新执行队列中的请求            requestQueue.forEach(cb => cb(res.data.access_token));            requestQueue = [];            // 5. 重试原请求            originalRequest.headers.Authorization = `Bearer ${res.data.access_token}`;            return request(originalRequest);          } else {            // refresh_token 也过期,需重新登录            removeToken();            ElMessage.error('登录状态已过期,请重新登录');            _window.location.href = '/login';          }        } catch (e) {          removeToken();          ElMessage.error('登录状态已过期,请重新登录');          _window.location.href = '/login';        } finally {          isRefreshing = false;        }      } else {        // 等待刷新 Token 完成后重试        return new Promise((resolve) => {          requestQueue.push((newToken: string) => {            originalRequest.headers.Authorization = `Bearer ${newToken}`;            resolve(request(originalRequest));          });        });      }    }    return Promise.reject(error);  });export { request };

三、关键注意事项(避坑指南)

1. 时间戳单位(秒 vs 毫秒)

  • JWT 的 exp 字段是秒级时间戳(Python time.time() 返回的是秒级浮点数,需转整数;JavaScript Date.now() 返回的是毫秒级,需除以 1000);
  • 错误示例:前端用 Date.now() 直接赋值给 exp → Token 会立即过期(因为毫秒级时间戳远大于秒级)。

2. 前端不要信任本地过期判断

  • 前端可解析 exp 字段做 “即将过期提示”(如剩余 5 分钟提示),但最终过期验证必须由后端完成(用户可篡改本地时间、伪造 Token)。

3. Refresh Token 的安全存储

  • Refresh Token 是 “终极凭证”,前端需存在 HttpOnly + Secure Cookie 中(防 XSS 攻击),禁止存在 LocalStorage;
  • 后端需将 Refresh Token 存储到 Redis(或数据库),登出 / 改密时主动删除,解决 JWT “无法主动销毁” 的问题。

4. 过期时间的合理选择

  • Access Token:15 分钟~2 小时(越短越安全,结合 Refresh Token 不影响体验);
  • Refresh Token:7 天~30 天(根据业务安全性要求调整)。

四、过期时间异常的排查

  1. Token 立即过期:检查 exp 时间戳单位(秒 / 毫秒)、服务器与客户端时间是否同步(建议用 NTP 同步服务器时间);
  2. Token 无法过期:检查验证时是否开启 verify_exp: True(JWT 库默认开启,但部分场景会手动关闭);
  3. Refresh Token 刷新失败:检查 Redis 中是否存在该 Token、exp 时间是否正确、Token 类型是否标记为 refresh。

总结(核心要点)

  1. 基础控制:通过 Payload 中的 exp 字段(秒级时间戳)设置过期时间,后端验证时自动校验;
  2. 体验优化:滑动过期(活跃时续期)或双 Token 机制(access_token 短期 + refresh_token 长期);
  3. 安全原则:Refresh Token 存在 HttpOnly Cookie,后端存储 Refresh Token 到 Redis 支持主动销毁;
  4. 避坑关键:注意时间戳单位(秒)、后端统一验证过期、禁止前端做核心过期判断。

💻 配套实训环境与动手练习

本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有