很多教程只讲 Token 怎么传、怎么校验,却没有说明它到底是怎么生成的。

先记住一个结论:Token 没有统一的生成算法。 不同 Token 的职责不同,生成方式也不同。

Token 的两类生成方式

一、先分清两类 Token

Token 大致分为两类:

类型

本质

常见例子

随机型 Token(Opaque Token)

一段无业务含义的安全随机字符串

Session ID、Opaque Access Token、Refresh Token

结构化 Token

数据加数字签名

JWT Access Token

因此,不能简单地说“Access Token 和 Refresh Token 都是随机数”。

  • Opaque Access Token 通常是安全随机数。

  • JWT Access Token 是声明数据加数字签名。

  • Refresh Token 通常采用安全随机字符串,但标准并未限制它只能使用这种格式。

OAuth 2.0 允许 Access Token 是服务端查询用的标识,也允许它自包含授权信息和签名;Refresh Token 对客户端通常是不透明字符串。

二、Session ID:使用密码学安全随机数

Session ID 本质上是一张临时登录凭证:谁拿到它,谁就可能以当前用户身份访问系统。

推荐生成流程:

操作系统随机源 → CSPRNG → 随机字节 → Base64URL / Hex → Session ID

这里的 CSPRNG 是“密码学安全伪随机数生成器”,常见实现包括:

  • Java:SecureRandom

  • Node.js:crypto.randomBytes()

  • 浏览器:crypto.getRandomValues()

OWASP 要求 Session ID 至少具有 64 位熵;自行实现时,建议直接生成至少 128 位随机数据。实际项目使用 16~32 字节较常见。

Java 示例

import java.security.SecureRandom;
import java.util.Base64;

public final class SecureTokenGenerator {
    private static final SecureRandom RANDOM = new SecureRandom();

    private SecureTokenGenerator() {
    }

    public static String generate(int byteLength) {
        byte[] bytes = new byte[byteLength];
        RANDOM.nextBytes(bytes);
        return Base64.getUrlEncoder()
                .withoutPadding()
                .encodeToString(bytes);
    }

    public static void main(String[] args) {
        // 32 字节 = 256 位随机数据
        System.out.println(generate(32));
    }
}

Spring Boot 使用容器 Session 时,通常不需要自己生成 JSESSIONID,Tomcat 等 Web 容器会负责生成和管理。

三、Opaque Access Token:也是安全随机字符串

Opaque Access Token 不携带可读取的用户信息,只充当查询键:

Access Token → Redis / 数据库 / 认证中心 → 用户、权限、过期时间

生成方式可以和 Session ID 相同:

import { randomBytes } from "node:crypto";

const accessToken = randomBytes(32).toString("base64url");

优点是容易立即吊销,权限变化也能及时生效;代价是资源服务器通常需要查询服务端状态。

四、JWT Access Token:不是随机数

JWT 的结构是:

Base64URL(Header) + Base64URL(Payload) + Signature

Payload 可能包含:

{
  "sub": "10001",
  "scope": "order:read",
  "exp": 1784000000,
  "jti": "随机唯一值"
}

JWT 的安全性主要来自:

  1. 签名密钥没有泄露;

  2. 服务端正确校验签名算法、签发者、受众和过期时间;

  3. Token 生命周期足够短。

JWT 的 Payload 默认只是编码,不是加密。不要在里面放密码、身份证号等敏感数据。

jti 可以使用安全随机数或 UUID,但这不代表整个 JWT 是随机生成的。

五、Refresh Token:通常使用更长的安全随机值

Refresh Token 生命周期较长,只用于向认证服务器换取新的 Access Token,不应发送给资源服务器。

常见做法:

Refresh Token = CSPRNG 生成 32~64 字节随机数据

Node.js 示例:

import { createHash, randomBytes } from "node:crypto";

const refreshToken = randomBytes(32).toString("base64url");

// 数据库保存哈希,而不是保存明文 Token
const refreshTokenHash = createHash("sha256")
  .update(refreshToken)
  .digest("hex");

为什么可以直接使用 SHA-256 保存 Refresh Token 哈希,而密码通常要使用 Argon2、bcrypt?

因为安全随机 Token 本身已经具有很高熵,攻击者无法像猜常见密码一样进行字典攻击。前提是 Token 必须由 CSPRNG 生成,并且长度足够。

Access Token 与 Refresh Token 的协作

更稳妥的策略是 Refresh Token Rotation

  1. 客户端使用 Refresh Token A 刷新;

  2. 服务端签发新的 Access Token 和 Refresh Token B;

  3. Refresh Token A 立即失效;

  4. 如果 A 再次出现,说明可能发生了复制或泄露,服务端撤销整条登录会话。

RFC 9700 要求公共客户端采用 Refresh Token Rotation 或发送方约束 Token 等机制,以检测刷新令牌重放。

六、UUID 能不能作为 Token?

UUID v4

UUID v4 的主体来自随机数,正确实现时大约具有 122 位随机性。普通系统用它作为 Session ID 通常不会轻易被猜中。

但职责上更建议这样区分:

UUID:用于标识一条记录
CSPRNG 随机串:用于生成秘密凭证

自行设计 Token 时,SecureRandomrandomBytes() 更直接,随机长度也更容易控制。

UUID v1、UUID v7、ULID 和雪花 ID

这些方案主要解决时间排序、分布式唯一性或数据库索引问题。它们可能暴露时间或结构信息,不适合直接作为秘密 Token。

它们可以用来生成:

  • 用户 ID

  • 订单 ID

  • Session 记录主键

  • 请求追踪 ID

不建议直接用来生成:

  • 登录 Token

  • Refresh Token

  • 密码重置 Token

  • 邮箱验证 Token

安全随机 Token 的生成链路

七、常见错误

1. 使用普通随机数

new java.util.Random().nextLong();
Math.random().toString(36);

这些随机数主要用于模拟、抽样或普通业务,不适合生成安全凭证。

2. 使用用户 ID 加时间戳

userId + timestamp

用户 ID 和登录时间通常都可以估算,攻击者能够缩小枚举范围。

3. 对可预测数据做 MD5 或 SHA-256

SHA-256(userId + timestamp)

哈希只能改变数据表现形式,不能凭空增加随机性。输入可预测,输出仍然可以被批量计算。

4. 只关注字符串长度

下面的字符串很长,但完全不安全:

user-10001-login-2026-07-13-12-00-00

安全性取决于有效随机熵,而不是肉眼看到的字符数量。

八、实际项目怎么选

场景

推荐生成方式

Web Session ID

框架默认实现;自行生成时使用 CSPRNG

Opaque Access Token

CSPRNG 生成 16~32 字节随机数据

JWT Access Token

Claims + 数字签名,设置较短有效期

Refresh Token

CSPRNG 生成 32~64 字节;服务端保存哈希并轮换

密码重置 Token

CSPRNG 生成随机值;短期、一次性使用

Session 数据库主键

UUID v4 / UUID v7 均可,但不要把主键直接当凭证

普通前后端分离系统可以采用:

Access Token:短期 JWT,约 10~30 分钟
Refresh Token:32 字节安全随机串,约 7~30 天
Session ID:UUID 或内部数据库主键,仅用于服务端标识会话记录

有效期没有统一答案,需要根据账号价值、使用频率、设备安全能力和风险控制策略调整。

九、最后总结

一句话记忆:

业务 ID 用 UUID;秘密凭证用 CSPRNG;需要自包含授权信息时才使用 JWT。

更准确地说:

  • Session ID:通常是密码学安全随机数;

  • Opaque Access Token:通常是密码学安全随机数;

  • JWT Access Token:声明数据加数字签名;

  • Refresh Token:通常是密码学安全随机数,并配合哈希存储和轮换机制。

参考资料