常见身份凭证与 Token 机制详解

之所以单独写出来,是因为我观察到很多博客里面,会对JWT,oauth2,乃至sa-token是什么产生一些混淆。

如果仅在后端开发学习的场景下,这问题不算是很大。但是如果供安全相关从业者学习,或者希望深入学习各种鉴权的从业者来说,还是容易产生误解的。

在开篇,我先以JWT,OAuth2和sa-token做一个简单的区分。这三个东西似乎都被用在鉴权领域里,为什么不算是同一类东西呢?

  • JWT(JSON Web Token): 它本质上是一种数据格式与签名规范(RFC 7519),用来安全地传输声明。它的结构非常标志性:
xml 复制代码
<Header>.<Payload>.<Signature>
  • OAuth2 / OIDC: 它们是协议与标准。
    • OAuth2 是一个授权框架,解决的是第三方应用访问受保护资源的问题。
    • OIDC(OpenID Connect) 则是基于 OAuth2 构建的身份认证协议,专门用于解决"用户是谁"的登录认证问题。
  • Sa-Token: 它是一个Java权限认证框架(类似于 Spring Security 或 Apache Shiro),专注于登录认证、权限验证、单点登录、踢人下线等业务场景的便捷实现。

我们以小明同学借助微信授权登入第三方平台A为例:

  • 第一阶段:授权/认证阶段(OIDC流程):
  1. 小明在平台 A 点击"微信登录",被重定向到微信认证服务。
  2. 微信确认小明身份后,颁发了一个 Authorization Code(授权码)给浏览器,由浏览器带给平台 A 的后端。(这个地方有一种说法是,微信直接把auth code给后端了。后面我考证了一下,准确的流程还是先给浏览器,再由浏览器带给后端。只是在语义上,可以认为是微信给平台后端的。)
  3. 平台 A 后端拿着这个Authorization Code再次请求微信服务,换取了Access TokenID Token
  4. 平台 A 利用 Access Token调用微信资源服务器,获取小明的昵称、头像、OpenID 等信息,并在平台 A 完成本地账号的注册或绑定,至此完成了基于 OIDC 的第三方登录。
  • 第二阶段:登陆状态维持:
  1. 为了维持小明同学的登录状态,此时,平台 A 还需再自己给小明颁发一个token维持小明的登录状态。

在这个故事里面,我们前后其实谈到了两个token,一个是微信颁发给平台后端的access token,一个是平台颁发给小明的token

那么这两个token和我们讨论的内容有什么关系呢?

  • 关于故事中微信或者其他第三方平台颁发的access token,它完全可以是JWT格式,也可以是其他的加密随机字符串的格式
  • 关于平台颁发给小明的token,它也可以是JWT格式,还可以是cookie/sessionid格式,还可以是放在document.cookie里面的JWT格式token。

相信这个例子,足以让我们区分token格式,协议标准和安全开发框架。

而在这篇博客里面,我们主要讨论的是前者,诸如JWT的token格式。

1. JWT / PASETO

1.1 JWT

JWT 是目前应用极其广泛的一种 Token 格式。

JWT主要由三个部分组成,每部分使用 Base64Url 编码后以.分隔

xml 复制代码
<Header>.<Payload>.<Signature>
  • 头部 (header)用于描述令牌元信息,包括签名算法和令牌类型。典型示例如下:
json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}
  • 载荷 (payload)包含实际要传递的信息,是 JWT 的核心部分,主要包括:

    • 注册声明(Registered Claims):标准字段,如
      • iss(Issuer)签发者
      • sub(Subject)主题,通常为用户唯一标识
      • aud(Audience)接收方
      • exp(Expiration Time)过期时间
      • nbf(Not Before)生效时间
      • iat(Issued At)签发时间
      • jti(JWT ID)唯一标识符
  • 公共声明(Public Claims):应用可自定义字段,用于存放角色、权限、部门 ID 等信息。

  • 私有声明(Private Claims):完全由应用定义的字段,用于携带业务特定信息,如用户偏好、单点登录标识等。

  • 签名(signature)主要由一下公式来声明,从算法上来看,很明显,这个是为了防篡改,以及完整性的。

scss 复制代码
Signature = HMACSHA256( base64UrlEncode(Header) + "." + base64UrlEncode(Payload), secret )

签名部分是JWT做无状态认证的关键,服务器无需保存任何会话的信息,只需要把header+payload丢进签名算法里面,然后比对和签名是否一致就行为了。

我们在一些简单的后端认证场景里面会看到这样的写法

c 复制代码
用户登录 ──> 后端签发 JWT(后端自身不落库保存) ──> 客户端存储并后续携带访问 ──> 后端验签(校验 userid, role, exp 等核心字段) ──> 正常放行响应

那么从那个JWT的原理上来看,JWT的安全性要考虑的问题就在于:

  • secret的安全,这包括secret的强度以及保密,及时轮换
  • 不在payload部分放置敏感的数据,base64并不算是加密
  • 正确且完整的验签,必须严格校验签名和过期时间(exp),防止算法被篡改(如常见的 alg: none 漏洞利用)或重放攻击。
  • 最后,也是常常容易忽略的,有设计有效的撤销机制。因为 JWT 本身是无状态的,在到达过期时间之前,单凭 Token 本身无法在服务端直接作废。常见的做法是结合 Redis 建立一个 Token 黑名单/注销列表,并在用户登出或账号被封禁时将 Token 放入黑名单中(配合定期清理过期的黑名单数据以节省内存)。

1.2 PASETO

PASETO之所以被放在这里,因为它算是一种更加先进的JWT。

PASETO出现的一种原因在于,JWT本身其实是安全的,但是容易被开发者不安全地实现------不安全的加密算法,算法混淆,混淆加密和签名把敏感信息放载荷。

PASETO对此提供一种默认安全的实现,或者说更容易安全实现,尽可能避免开发者错误选择。

格式上PASETO如下

复制代码
version.purpose.payload.footer
  • version: 很显然是版本号
  • purpose: 用途
  • payload: 载荷数据
  • footer: 附加信息

其中具有特色的是明确的purpose,有且仅有两种参数选择:localpublic

  • local模式下是加密的token,类似于AES-GCM,内容保密,token之中的载荷攻击者无法看到
  • public模式下是数字签名token,类似于JWT RS256,内容公开,但是无法修改。

更详细的格式区分:

vbnet 复制代码
local:
v4.local.<encrypted_payload>.<footer>
public:
v4.public.<payload(base64明文)>.<signature>.<footer>
被签名的部分:
PAE(
    "v4.public",
    payload,
    footer,
    implicit_assertion
)

总结来说就是,相对于JWT,PASETO具有的优势有:默认安全的实现;可以通过local模式进行真正的加密;有固定的加密算法,防止算法降级;然后例如V4版本使用更为现代的加密算法(例如Ed25519,XChaCha20)。

而从其原理上来看,很明显应该注意的安全问题有:

  • PASETO依旧没有撤销机制,需要配合Redis黑名单等机制使用
  • 需要确保密钥的安全性
  • 完善的验签机制
  • 多一点的是,不要误以为PASETO更加安全,就可以在payload里面放敏感数据。public情况下,payload依旧是不加密的。

总得来说,PASETO的使用目前应该是没有JWT多的,生态也没有那么完善的。像OAuth生态下的支持目前也不是那么的好。

究竟是JWT还是PASETO要看需求而定,正确实现的JWT也不失安全性。

2. Opaque Token / Reference Token(不透明的token/引用token)

与自带明文信息的 JWT 不同,Opaque Token(不透明 Token) 本身是一个没有任何业务含义的随机字符串。(例如a8f91bc729d82ff0192

在国内这种token更为场景的称呼是随机tokenUUID 令牌,或者直接结合Redis session来用。

具体工作流程:

  1. 用户登录成功后,后端生成一个高熵的随机字符串作为 Token,并将该 Token 与用户的会话信息(如 User ID、权限、登录状态等)绑定存入 Redis 或数据库中。
  2. 客户端在后续请求中将该 Token 通过请求头或 Cookie 带给后端。
  3. 后端拿到 Token 后,直接去 Redis 或数据库中进行查询:如果查不到或者已过期,则说明未登录或失效;如果查询成功,则取出关联的用户信息进行后续业务处理。

它的底层设计理念和传统的 Cookie/Session 机制非常相似------状态保存在服务端,客户端只持有一个用于检索的凭证。

对比于JWT,opaque token / reference Token 的好处很明显,它的载荷里面不太会被错误放入敏感信息;另外撤销更加容易。

缺陷在于,所有的token都需要存储在Redis/数据库中,需要考虑更高的开销。当然,这一部分是后端考虑的事情。从安全性的角度,我们可能需要考虑更多:

  • 作为一个高熵的随机字符串,它的生成算法需要足够的安全(安全的伪随机数生成算法),它的长度也需要足够(128位以上),以避免被暴力猜解或者碰撞。(如 Java 的 SecureRandom、Node.js 的 crypto.randomBytes)
  • 因为需要存储在Redis/数据库之中,token作为一个敏感信息,需要享有和密码一样的待遇。最好不要明文直接地存储,而是存储其hash值,且使用安全的hash算法。这样即便被脱库了,token也不会严重泄露。这一安全考量同样适用于各类需要持久化存储的 Token。
  • 最后是严格的生命安全周期和撤销机制。

3. API Key / AKSK

3.1 API Key

这个东西在大模型调用,或者其他第三方服务调用的场景之下相当常见。

从本质上来看,API Key 是由服务器生成的一串高熵随机字符串。当用户携带 API Key 访问服务时,服务端会根据该 Key 查询并校验对应的信息,例如访问范围、权限、过期时间及调用额度等。

典型的校验流程大概如下:

markdown 复制代码
1. 从请求头读取 API Key
2. 对 API Key 做哈希计算
3. 在数据库中查找对应记录
4. 检查 Key 是否存在
5. 检查是否被禁用或过期
6. 检查接口权限
7. 检查 IP、来源和调用频率
8. 记录日志和计费信息
9. 允许或拒绝请求

从本质上讲,API Key 类似于一种不透明令牌(Opaque Token),那么从安全性的角度,我们分为两个方面考虑,一方面是服务提供方,另一方面是服务使用方。

  • 对于服务提供方而言:

    • 我们抓住关键词"高熵随机字符串",很好,这告诉我们,必须使用密码学安全伪随机数生成器(CSPRNG),以及字符串的长度要足够(建议至少 128 位到 256 位随机熵,编码后通常表现为 32 位以上的字符串)。
    • 我们再抓住需要服务器数据库进行存储这个关键点,这告诉我们,不要明文存储这个敏感数据,要使用和密码一样的待遇:安全的现代哈希算法,比对时只比对哈希。
    • 合理的生命周期管理,合理的撤销机制......
    • 必须通过 HTTP 请求头(如 Authorization 或自定义 Header)传递,尽量不要放在 URL 查询参数(Query String)或请求体中,以防因浏览器历史记录、Referer 泄露或网关访问日志记录而导致明文暴露。
  • 对于服务使用方而言:

    • 它需要享有`secret的待遇,不要硬编码,注意保密。
    • 仅在安全的协议下传输
    • 必要的情况下,需要有合适的自动轮换机制

3.2 AKSK (Access Key / Secret Key)

K/SK 是一种比普通 API Key 更为严格、常用于云厂商(如 AWS、阿里云、腾讯云等)的开放接口认证方式。

一种粗糙的理解是

ini 复制代码
AK = 用户名 / 钥匙编号(公开的)
SK = 密码 / 真正的钥匙(秘密的)

需要注意的是,它与传统的"账号密码表单式登录"有本质区别:在传统登录中,Password 通常需要在网络中传输(即使加密)。

而在 AK/SK 认证机制下,SK 绝不会在网络中传输。这也是 AK/SK 相比普通 API Key 更为安全的核心原因。

流程具体而言就是:

sql 复制代码
--------------
客户端(Client)
AK + SK + 请求内容(Method、URI、Headers、Body 等)
                        ↓
                 计算加密签名(Signature)
                        ↓
发送:AK + 签名 + 时间戳 + 随机数(Nonce)给服务端
---------------
服务器端
1. 从请求中读取 AK
2. 根据 AK 在数据库/KMS 中查找到对应的 SK(以及关联的用户/权限)
3. 检查时间戳与随机数,防范重放攻击
4. 使用与客户端完全相同的规则,拼接并生成"待签名字符串"
5. 使用查出来的 SK 重新计算服务端签名
6. 严格比对客户端签名与服务端签名是否一致
7. 检查接口权限、调用频率(Rate Limiting)
8. 允许或拒绝请求

我们依旧从两个视角讨论安全问题:

  • 服务提供方:

    • SK的生成安全(密码学安全的随机数生成器+足够长)
    • SK的存储安全(这个地方相对于API Key的麻烦指出在于,SK没法只存哈希,否则将无法进行签名校验,这会更依赖数据库的安全。因此服务端必须采用**高强度的对称加密(如 AES-GCM)**将 SK 加密后存储在数据库中,并配合密钥管理服务(KMS)严格保护主密钥,防止数据库拖库导致 SK 整体泄露。)
    • 合理的生命周期管理,合理的撤销机制......
  • 服务使用方:

    • SK的保密
    • 不要误传SK
    • 必要的情况下,需要有合适的自动轮换机制

4. SAML Assertion

其实,这一部分原本并不想讨论。SAML 本身是一套协议,讨论 SAML Assertion 就很难完全绕开 SAML 协议,这会有点偏离本文介绍常见 Token 的核心。

但是,最终这个部分还是被加上了,因为 SAML Assertion 确实是一种基于 XML 的安全断言,也常被作为安全令牌使用。

举个例子,简单说明一下 SAML 协议的工作流程:

text 复制代码
小明同学需要使用学校的教务系统(服务提供商,SP)。他尝试登录教务系统,
页面被重定向到了学校的统一身份认证平台(身份提供商,IdP)。

小明在 IdP 完成身份认证后,IdP 生成一个 SAML Assertion(断言),其中可能包含:

用户名:小明
身份:学生
所属组织:某某学校
......
[IdP 数字签名]

随后,浏览器将包含该断言的 SAML Response 提交给教务系统(SP)。
教务系统验证签名及断言中的有效期、接收方等条件后,
为小明创建本地登录会话。随后,小明便可以正常使用教务系统。

这个流程听起来和 OAuth2/OIDC 有点像,确实存在一些相似之处。

SAML Assertion 由 IdP 签发,用于向 SP 声明某个主体的身份认证情况以及相关属性。例如,它可以声明小明已经通过身份认证、小明的身份标识是什么、所属部门是什么。至于这些属性最终对应哪些业务权限,通常由 SP 根据本地规则进行映射和判断。

OAuth 2.0 中的 Access Token 本质上是一种授权凭证,代表客户端获得了访问特定受保护资源的权限,它本身不直接等同于用户身份信息,常用于客户端代表用户或以自身身份调用资源服务器。若需要在 OAuth2 体系之上完成用户身份认证,通常需要结合 OpenID Connect,并使用 ID Token 表达身份认证结果。

SAML Assertion 的一个典型结构如下。

它通常由可信的身份提供商(IdP)签发,是一份包含主体、限制条件以及身份或属性声明的 XML 文档:

xml 复制代码
<Assertion>

  <Issuer>
    https://idp.example.com
  </Issuer>

  <Subject>
    <NameID>Alice</NameID>
  </Subject>

  <Conditions
      NotBefore="2026-08-03T10:00:00Z"
      NotOnOrAfter="2026-08-03T10:05:00Z">
    <AudienceRestriction>
      <Audience>https://sp.example.com</Audience>
    </AudienceRestriction>
  </Conditions>

  <AuthnStatement>
    ...
  </AuthnStatement>

  <AttributeStatement>
    ...
  </AttributeStatement>

  <Signature>
    <SignedInfo>
      ...
    </SignedInfo>
    <SignatureValue>
      ...
    </SignatureValue>
    <KeyInfo>
      ...
    </KeyInfo>
  </Signature>

</Assertion>

整个签名过程可以简化理解为:

text 复制代码
1. 对需要保护的 XML 数据执行规定的转换和规范化处理。
2. 对被引用的数据计算摘要,并将结果写入 DigestValue。
3. 对 SignedInfo 元素进行规范化。
4. IdP 使用私钥和指定的签名算法,对规范化后的 SignedInfo 生成数字签名,
   并将结果写入 SignatureValue。
5. SP 使用 IdP 的公钥验证 SignatureValue,同时重新计算并校验各个引用数据的摘要。
6. 签名验证通过后,SP 还需要继续检查签发者、有效期、Audience、
   Recipient、SubjectConfirmation 等条件。

由于 XML 具有较强的表达能力,SAML Assertion 可以承载身份标识、身份认证信息、组织属性和角色等丰富内容。当然,结构越复杂,其生成、传输和解析的开销通常也会相应增加。

那么,从安全层面上,仅看 SAML Assertion 的部分,值得考虑的问题与 JWT 存在一些相似之处:

  • SAML Assertion 通常会进行数字签名,但数字签名并不等于加密。因此,不应仅依赖签名来保护其中的敏感信息。如果确实需要传递机密信息,可以使用 XML Encryption,并且传输过程仍应使用 TLS。
  • SAML Assertion 通常具有较短的有效期,但其本身一般没有类似服务端会话那样的即时撤销能力。因此,需要严格校验 NotBeforeNotOnOrAfterAudienceRecipientInResponseTo 等字段,并结合短有效期、请求关联和断言使用记录等措施防止重放。如果需要立即终止用户访问,还需要同时处理 SP 本地会话或其他服务端状态。
  • 整体安全性高度依赖 IdP 私钥及信任配置的安全性。
  • SP 不能只检查"签名是否正确",还必须确认签名保护的确实是随后被业务逻辑使用的那个 Assertion,并完整校验断言的签发者、接收方、时间限制和主体确认条件。

总结

从我们讲的上述的例子来看,其实我们可以观察到,如何保证某种token的安全,其实是和token本身的性质有关的。

这涉及到:

  • 后端服务器存不存?(有状态无状态?)
    • 无状态算法,常常严重依赖于secret的安全性。存储的安全吗?有没有硬编码?长度够不够?随机轮换?
    • 有状态算法,常常需要后端Redis/数据库进行存储,要考虑怎么存(安全哈希?无法哈希的对称加密?)
  • 怎么生成的?(token的生成算法)
    • 载荷加不加密?不加密就不能够放敏感信息
    • 不透明 Token(Opaque Token / Reference Token),那生成随机数算法是否密码学安全?token够不够长?
  • 浏览器存哪里?(sessionStorage / Cookie /localStorage ?)这一部分要复杂的多,这里仅仅简述。
    • 存document.cookie,有没有考虑HttpOnly,samesite,csrf-token等手段防止XSS和CSRF?
    • 存localStorage,有没有考虑严格的CSP防xss?

依据这些,我们可以分析一个token设计是否安全,以及要考虑哪些安全。

相关推荐
杨先生哦2 小时前
【2026热端攻防系列 11/12】前端AI风控攻防实战:验证码缺陷、人机验证绕过、智能爬虫对抗与企业智能风控加固方案
前端·人工智能·笔记·爬虫·安全
AI行业学习2 小时前
Claude Code + cc-switch + Git + Node.js 一站式完整安装配置教程【8.3】
git·python·安全·前端框架·node.js·html·notepad++
AI行业学习2 小时前
Claude Code + cc-switch + Git + Node.js 一站式完整安装配置教程(2026最新·国内可用版)
人工智能·git·python·安全·node.js·html·notepad++
汽车仪器仪表相关领域2 小时前
NHA-604/605汽车排放气体测试仪:00级国标计量|多动力车型兼容|移动/固定双工况|智能联网尾气检测设备
大数据·人工智能·功能测试·安全·汽车·压力测试·可用性测试
志栋智能2 小时前
超自动化安全中的自适应防护与自愈能力
网络·安全·自动化
糖果店的幽灵5 小时前
大模型测评DeepEval快速入门-安全与通用指标详解
人工智能·安全·langgraph·大模型测评·deepeval
网络研究院16 小时前
LastPass 发布紧急安全预警:针对主密码的活跃网络钓鱼攻击正在进行中
网络·安全·黑客·攻击·漏洞·风险·钓鱼
himobrinehacken17 小时前
揭秘Windows程序启动的神秘之旅
c++·安全
TunerT_TQ19 小时前
【智能体安全治理|专栏第9期】从“一堆规则”到“数字宪法”:智能体治理的下一个阶段
java·开发语言·安全·开源治理·大模型安全·ai基础设施·智能体安全