文章目录
-
- 概念介绍
-
- [🔑 什么是 Access Token(访问令牌)?](#🔑 什么是 Access Token(访问令牌)?)
- [📜 什么是 JWT(JSON Web Token)?](#📜 什么是 JWT(JSON Web Token)?)
-
- [JWT 的结构](#JWT 的结构)
- [💻 具体代码示例(Python 实战)](#💻 具体代码示例(Python 实战))
- [📌 总结与最佳实践](#📌 总结与最佳实践)
- [签名密钥(Secret Key)如何生成](#签名密钥(Secret Key)如何生成)
-
- [🛠️ 方法一:使用命令行工具快速生成(最推荐)](#🛠️ 方法一:使用命令行工具快速生成(最推荐))
- [💻 方法二:在编程语言中动态生成](#💻 方法二:在编程语言中动态生成)
- [⚙️ 方法三:使用在线生成工具](#⚙️ 方法三:使用在线生成工具)
- [🔒 密钥生成与管理的核心原则](#🔒 密钥生成与管理的核心原则)
- [JWT和Access Token的关系](#JWT和Access Token的关系)
-
- [🎭 核心关系:抽象概念 vs 具体实现](#🎭 核心关系:抽象概念 vs 具体实现)
- [🔍 它们到底是什么关系?](#🔍 它们到底是什么关系?)
-
- [1. 包含与被包含的关系](#1. 包含与被包含的关系)
- [2. 为什么现在大家总把两者混为一谈?](#2. 为什么现在大家总把两者混为一谈?)
- [⚖️ 直观对比:普通 Access Token vs JWT 格式的 Access Token](#⚖️ 直观对比:普通 Access Token vs JWT 格式的 Access Token)
- [📌 总结](#📌 总结)
- [Bearer 是什么意思](#Bearer 是什么意思)
-
- [🎟️ Bearer 的核心含义:认票不认人](#🎟️ Bearer 的核心含义:认票不认人)
- [📝 标准格式](#📝 标准格式)
- [💡 为什么要加 "Bearer" 这个词?](#💡 为什么要加 "Bearer" 这个词?)
- [🔒 安全提醒](#🔒 安全提醒)
- [如何将Token存放在HttpOnly Cookie](#如何将Token存放在HttpOnly Cookie)
-
- [🛠️ 第一步:后端设置并下发 HttpOnly Cookie](#🛠️ 第一步:后端设置并下发 HttpOnly Cookie)
- [💻 第二步:前端正常发送请求(跨域需特殊处理)](#💻 第二步:前端正常发送请求(跨域需特殊处理))
- [🔍 第三步:后端验证 Token](#🔍 第三步:后端验证 Token)
- [⚠️ 注意事项与避坑指南](#⚠️ 注意事项与避坑指南)
概念介绍
JWT(JSON Web Token)和 Access Token(访问令牌)是现代网络应用中实现身份验证与授权的核心概念。为了让你全面理解,我们将分别深入介绍它们,并给出具体的代码示例。
🔑 什么是 Access Token(访问令牌)?
Access Token 就像是一张**"数字门票"或"临时通行证"**。当用户成功登录系统后,服务器会颁发一个 Access Token 给客户端。在后续的请求中,客户端只需携带这张"门票",服务器就能识别用户的身份并判断其是否有权访问受保护的资源(如查看个人资料、获取订单数据等)。
- 核心作用 :用于授权(Authorization),即告诉服务器"我有权做这件事"。
- 常见格式 :它可以是一个不透明的随机字符串,但如今最主流的格式是 JWT。
- 生命周期:为了安全起见,Access Token 的有效期通常较短(例如 15 分钟到 2 小时)。过期后,用户需要重新登录或通过刷新令牌(Refresh Token)来获取新的 Access Token。
在实际的 HTTP 请求中,Access Token 通常被放在请求头(Header)中,格式如下:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
📜 什么是 JWT(JSON Web Token)?
JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地以 JSON 对象的形式传输信息。你可以把它理解为一种特定格式的 Access Token。
JWT 最大的特点是无状态 和自包含。这意味着服务器不需要在数据库或内存中存储会话信息,因为 JWT 本身就包含了所有必要的用户信息和防篡改签名。
JWT 的结构
一个完整的 JWT 由三部分组成,中间用点号(.)分隔,看起来像这样:xxxxx.yyyyy.zzzzz。
这三部分分别是:头部(Header) 、载荷(Payload) 和 签名(Signature)。
1. 头部(Header)
头部通常包含两部分:令牌的类型(即 JWT)以及所使用的签名算法(如 HMAC SHA256 或 RSA)。
json
{
"alg": "HS256", // 签名算法
"typ": "JWT" // 令牌类型
}
这部分内容经过 Base64Url 编码后,形成了 JWT 的第一部分。
2. 载荷(Payload)
载荷用来存放实际需要传递的数据(称为声明 Claims)。它包含三种类型的声明:
- 注册声明 :预定义的标准字段,如
iss(签发者)、exp(过期时间)、sub(面向的用户/主题)、iat(签发时间) 等。 - 公共声明:使用者自定义的公开字段。
- 私有声明 :通信双方约定的自定义字段,例如
user_id、role等。
⚠️ 重要安全提醒 :载荷只是经过了 Base64Url 编码 ,而不是加密!任何拿到 JWT 的人都可以轻松解码并看到里面的内容。因此,绝对不要在 JWT 中存放密码、银行卡号等敏感信息。
载荷示例:
json
{
"sub": "1234567890",
"name": "张三",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
3. 签名(Signature)
签名是 JWT 的核心,用于验证消息在传输过程中未被篡改,并确认签发者的身份。
以 HS256 算法为例,签名的生成方式是:将编码后的 Header 和 Payload 用点号连接,再配合服务端绝密的密钥(Secret)进行加密运算。
text
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
如果黑客试图修改 Payload 中的权限(比如把 role 从 user 改为 admin),由于他没有服务端的 secret,生成的签名就会与服务端计算的不一致,服务器便会拒绝该请求。
💻 具体代码示例(Python 实战)
在实际开发中,我们通常会使用成熟的第三方库来生成和验证 JWT。以下使用 Python 的官方推荐库 PyJWT 来演示。
首先安装依赖:
pip install pyjwt
python
import jwt
import datetime
# 实际项目中,SECRET_KEY 应该存放在环境变量中,绝不能硬编码
SECRET_KEY = "your-super-secret-key-keep-it-safe"
# 1. 生成 JWT (作为 Access Token)
def create_access_token(user_id, role):
# 准备载荷(Payload)
payload = {
"user_id": user_id,
"role": role,
# 设置过期时间为当前时间往后推1小时
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1),
# 签发时间
"iat": datetime.datetime.now(datetime.timezone.utc)
}
# 生成 JWT,指定算法为 HS256
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return token
# 2. 验证和解码 JWT
def verify_access_token(token):
try:
# 解码并验证(库会自动检查 exp 是否过期,以及签名是否正确)
decoded_payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
print("✅ 令牌验证通过,解析出的用户信息:", decoded_payload)
return decoded_payload
except jwt.ExpiredSignatureError:
print("❌ 令牌已过期,请重新登录")
except jwt.InvalidTokenError as e:
print(f"❌ 无效的令牌(可能被篡改): {e}")
# --- 测试运行 ---
if __name__ == "__main__":
# 模拟用户登录,生成令牌
access_token = create_access_token(user_id=1001, role="admin")
print("生成的 Access Token (JWT):", access_token)
print("-" * 50)
# 模拟客户端携带令牌访问接口,服务端进行验证
verify_access_token(access_token)
📌 总结与最佳实践
- JWT vs Access Token:Access Token 是一个抽象的概念(通行证),而 JWT 是实现这个通行证的一种非常流行的技术标准(自带信息的通行证)。
- 无状态优势:JWT 非常适合分布式系统和微服务架构,因为各个服务只需要共享同一个密钥即可验证令牌,无需频繁查询数据库或共享 Session。
- 安全建议 :
- 始终使用 HTTPS 传输,防止令牌在网络中被截获。
- 设置合理的短期过期时间(如 1 小时)。
- 前端存储时,推荐使用安全的
httpOnly Cookie而非localStorage,以防范 XSS 攻击。 - 确保签名密钥(Secret Key)足够复杂且妥善保管。
签名密钥(Secret Key)如何生成
生成 JWT 的签名密钥(Secret Key)绝不能像普通密码那样随意设定(比如 "123456" 或 "mysecret")。为了保证系统安全,必须使用**密码学安全的伪随机数生成器(CSPRNG)**来生成足够长且完全随机的字符串。
特别是当你使用主流的 HS256 (HMAC-SHA256) 算法时,JWT 规范强制要求密钥长度至少为 256 位(即 32 字节) 。如果密钥过短,在代码运行时通常会直接抛出 WeakKeyException 异常。
以下为你提供几种在不同场景下生成高安全性 Secret Key 的方法:
🛠️ 方法一:使用命令行工具快速生成(最推荐)
如果你熟悉终端操作,这是最快且极其安全的方式。
-
macOS / Linux 用户(使用 OpenSSL) :
直接在终端执行以下命令,它会生成一个 32 字节的 Base64 编码随机字符串,非常适合作为 JWT 密钥:
bashopenssl rand -base64 32示例输出:
8v/4kL2m9P0qR1sT3uV5wX7yZ9aB1cD3eF5gH7iJ9kL= -
Node.js 环境用户 :
如果你在开发 Node.js 项目,可以直接在终端运行这行代码来生成 32 字节的十六进制密钥:
bashnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))"示例输出:
a47f7c20e4f9a87c6d0af6e6c8c4bc25d2a8e7c4a9f5c30f72db7d9a48f1c3d2
💻 方法二:在编程语言中动态生成
如果你需要在程序启动时自动生成并保存密钥,可以使用各语言自带的加密库。以下是 Java 和 Python 的安全生成示例:
Java 示例(使用 JJWT 官方推荐的 Keys 工具类):
java
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Base64;
// 方式1:让库自动为你生成符合 HS256 要求的 256 位安全密钥
SecretKey key = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
// 方式2:将生成的二进制密钥转为 Base64 字符串,方便存入配置文件
String base64Key = Base64.getEncoder().encodeToString(key.getEncoded());
System.out.println("生成的安全密钥: " + base64Key);
Python 示例(使用 secrets 模块):
python
import secrets
# 生成 32 字节(256位)的随机十六进制字符串
secure_key = secrets.token_hex(32)
print(f"生成的安全密钥: {secure_key}")
⚙️ 方法三:使用在线生成工具
如果你不想敲命令也不想写代码,可以使用专业的在线密码生成器。只需确保设置如下参数:
- 长度:至少 32 个字符(建议 44 字符以上,如果是 Base64 格式)。
- 字符集:勾选所有选项(大写字母、小写字母、数字、特殊符号)。
- 注意:出于最高级别的安全考量,生产环境的绝密密钥建议优先在本地离线生成。
🔒 密钥生成与管理的核心原则
无论你使用哪种方法,请务必遵守以下安全红线:
- 严禁硬编码 :绝对不要把生成的密钥直接写死在源代码里(例如
SECRET_KEY = "xxx")。一旦代码上传到 GitHub 等公开仓库,你的整个认证系统将瞬间沦陷。 - 使用环境变量 :将生成好的密钥存放在
.env文件或服务器的环境变量中(如JWT_SECRET=你生成的超长随机字符串),在代码中通过读取环境变量的方式来调用。 - Base64 只是编码不是加密 :生成的 Base64 字符串(如
SGVsbG9Xb3JsZA==)任何人都能解码看到原文。它只是为了方便传输和存储,本身并不具备保密性,因此这个字符串必须像保护数据库密码一样严加看管。
JWT和Access Token的关系
理解 Access Token 和 JWT 的关系,其实只需要记住一句话:Access Token 是"职位/角色",而 JWT 是实现这个角色的其中一种"具体人选"。
为了让你彻底搞懂它们的区别与联系,我们可以通过一个生活化的类比、一张对比表以及技术演进的角度来详细拆解。
🎭 核心关系:抽象概念 vs 具体实现
- Access Token(访问令牌)是一个抽象的概念 。它指的是在 OAuth 2.0 等授权协议中,客户端用来换取服务器资源的那串"凭证"。协议只规定了它的作用(证明你有权限),但没有规定它必须长什么样。
- JWT(JSON Web Token)是一种具体的技术标准/格式。它定义了令牌内部该如何编码、如何签名、包含哪些字段。
💡 生活化类比:
- Access Token 就像是 "门票" 这个词。它的作用是让你进入游乐园。
- JWT 就像是 "印有二维码和防伪水印的纸质票"。这是一种非常流行的门票制作形式。
- 但是,"门票"也可以是其他形式,比如老式的"打孔硬卡纸"或者"手环"。
🔍 它们到底是什么关系?
1. 包含与被包含的关系
并不是所有的 Access Token 都是 JWT,但 JWT 经常被用作 Access Token。
- Access Token 的表现形式通常有两种:
- 不透明字符串 (Opaque String): 比如
4a8f7b2c1d9e。这种令牌本身没有任何意义,就像一把随机的钥匙。服务器收到后,必须去数据库里查:"这把钥匙对应哪个用户?过期了吗?"(传统 Session 模式常用)。 - 自包含字符串 (Self-contained): 也就是 JWT 。比如
eyJhbG...xxxx.yyyy。服务器收到后,不需要查数据库,直接通过数学公式(验签)就能算出它是谁、有没有被篡改。
- 不透明字符串 (Opaque String): 比如
2. 为什么现在大家总把两者混为一谈?
因为在现代前后端分离、微服务架构的开发中,使用 JWT 作为 Access Token 是最主流、最高效的方案。所以很多开发者在日常交流时,习惯性地把 Access Token 等同于 JWT,但在严谨的技术定义上,它们不能划等号。
⚖️ 直观对比:普通 Access Token vs JWT 格式的 Access Token
假设两个用户都要访问"获取订单信息"的接口,他们携带的 Access Token 处理方式完全不同:
| 特性 | 传统不透明 Access Token | JWT 格式的 Access Token |
|---|---|---|
| 外观示例 | gAAAABBBBCCCC12345 (随机乱码) |
eyJhbGciOiJ...xxxxx.yyyyy (三段式结构) |
| 服务器验证方式 | 查库:拿着 Token 去数据库/Redis 查询对应的用户信息。 | 计算:本地通过算法解密并验证签名,无需查库。 |
| 性能 | 每次请求都要访问数据库,高并发时数据库压力大。 | 纯CPU计算,速度极快,非常适合分布式系统。 |
| 包含信息 | 无(信息全在服务端数据库里)。 | 有(Payload 里自带 UserID、角色等信息)。 |
| 注销难度 | 容易(直接在数据库删掉这条记录即可)。 | 较难(因为无状态,通常需要配合黑名单或短有效期)。 |
📌 总结
你可以这样向别人介绍它们的关系:
- Access Token 是为了保护 API 而设立的一道关卡,任何能通关的凭证都叫 Access Token。
- JWT 是目前最流行的一种通关凭证样式。它把用户信息直接写在了凭证上(并且加了防伪印章),让守门员(服务器)一眼就能识别,不用再去翻花名册(查数据库)。
所以在实际工作中,当你的领导或同事说"生成一个 Access Token"时,大概率他们指的就是"生成一个 JWT 字符串"。
Bearer 是什么意思
Bearer 翻译成中文就是**"持有者"或"携带者"**的意思。
在 HTTP 身份验证(尤其是使用 JWT 作为 Access Token)中,它代表了一种非常直接的授权逻辑:"谁手里拿着这个令牌(Token),服务器就认为谁是合法用户。"
🎟️ Bearer 的核心含义:认票不认人
当你在请求头中看到 Authorization: Bearer <你的JWT> 时,它的意思是:
"你好服务器,我是这个令牌的持有者(Bearer)。我把这张门票交给你看,请允许我访问受保护的资源。"
服务器收到后,不会去核实"你是不是最初登录的那个人"(比如不会让你再输一次密码或做人脸识别),而是只检查你手里的这张票据(JWT)是否真实、是否在有效期内。只要票据是真的,服务器就放行。
📝 标准格式
在 HTTP 请求的 Header 中,Bearer 认证的标准写法是固定的:
http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
- Authorization:这是标准的 HTTP 请求头字段名,表示"我要进行身份验证"。
- Bearer:这是认证方案(Scheme),告诉服务器:"我后面跟着的是一个令牌,请你按'持有者'的规则来验证它"。
- eyJhb... :这就是你的 JWT(Access Token)。注意
Bearer和Token之间必须有一个空格。
💡 为什么要加 "Bearer" 这个词?
你可能会问,直接发 Token 不行吗?为什么要多写一个单词?
这是因为 HTTP 协议支持多种身份验证方式。加上 Bearer 是为了明确告诉服务器当前使用的是哪种规则。常见的还有:
- Basic :
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=(通常用于传用户名和密码的 Base64 编码) - Digest:一种比 Basic 更安全的摘要认证方式。
所以,Bearer 就像是一个"暗号前缀",服务器一看到它,就知道:"哦,接下来要处理的是一个 JWT/Access Token,我要去验签了。"
🔒 安全提醒
正因为 Bearer 认证是**"认票不认人"**,所以保护你的 Access Token(JWT)至关重要!
- 如果黑客通过抓包、XSS 攻击等手段窃取了你的 JWT,他就可以伪装成你(成为合法的 Bearer),随意调用你的接口。
- 这也是为什么现代 Web 开发强烈建议使用 HTTPS (防止半路被抢走票据)以及将 Token 存放在 HttpOnly Cookie(防止被网页里的恶意脚本偷走)的原因。
如何将Token存放在HttpOnly Cookie
将 Token 存放在 HttpOnly Cookie 中,是保护用户身份凭证(如 JWT)免受 XSS(跨站脚本)攻击的最佳实践。
实现这一点的核心原则是:Token 绝对不能由前端 JavaScript 来设置或读取,必须完全交由后端在服务端通过 HTTP 响应头下发,并由浏览器自动管理。
下面为你详细拆解从后端到前端的完整落地流程:
🛠️ 第一步:后端设置并下发 HttpOnly Cookie
当用户登录成功时,后端在返回的 HTTP 响应中,通过 Set-Cookie 响应头将 Token 写入浏览器的 Cookie 中,并附加严格的安全属性。
核心安全属性说明:
- HttpOnly :这是最关键的属性。它禁止前端 JavaScript(如
document.cookie)访问该 Cookie,从根本上杜绝了 XSS 窃取 Token 的风险。 - Secure:强制要求该 Cookie 只能在 HTTPS 协议下传输,防止在网络传输过程中被中间人窃听。
- SameSite :限制第三方网站发起的请求携带此 Cookie。建议设置为
Strict或Lax,能有效防御 CSRF(跨站请求伪造)攻击。 - Path :通常设置为
/,表示该 Cookie 在整个域名下的所有路径都有效。
代码示例(以 Node.js/Express 为例):
javascript
app.post('/api/login', (req, res) => {
// 1. 验证用户账号密码...
// 2. 生成 JWT Token...
const token = generateJWT(user);
// 3. 将 Token 放入 HttpOnly Cookie 中下发给客户端
res.cookie('access_token', token, {
httpOnly: true, // 核心:JS无法读取,防XSS
secure: process.env.NODE_ENV === 'production', // 生产环境仅HTTPS传输
sameSite: 'strict', // 严格限制跨站请求,防CSRF
maxAge: 24 * 60 * 60 * 1000, // 有效期1天
path: '/' // 全站生效
});
res.status(200).json({ message: '登录成功' });
});
💻 第二步:前端正常发送请求(跨域需特殊处理)
一旦后端设置了 HttpOnly Cookie,浏览器就会接管后续工作。
- 同域场景 :当前端和后端部署在同一个域名下时,浏览器会在每次向该域名发起 HTTP 请求时,自动在请求头中带上这个 Cookie。前端代码无需做任何额外操作,直接调用接口即可。
- 跨域场景 :如果前后端分离且存在跨域(例如前端在
www.example.com,后端 API 在api.example.com),你需要在前端的请求库中显式开启"携带凭证"的配置。
前端跨域请求配置示例(使用 Axios):
javascript
import axios from 'axios';
// 全局配置:允许跨域请求自动携带 Cookie (即 HttpOnly Token)
axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'https://api.example.com';
const getUserInfo = async () => {
// 浏览器会自动把 access_token 放在 Cookie 请求头中发往后端
const res = await axios.get('/user/info');
return res.data;
};
🔍 第三步:后端验证 Token
当受保护的接口收到请求时,后端直接从请求头的 Cookie 字段中提取 Token 并进行验签,而不是去提取 Authorization 头。
后端验证逻辑示例:
javascript
function authMiddleware(req, res, next) {
// 从请求的 Cookie 中获取 Token
const token = req.cookies.access_token;
if (!token) {
return res.status(401).json({ message: '未提供身份凭证' });
}
try {
// 验证 Token 的有效性(是否过期、签名是否正确)
const decoded = verifyJWT(token);
req.user = decoded; // 将解析出的用户信息挂载到请求对象上
next();
} catch (error) {
return res.status(401).json({ message: '身份凭证无效或已过期' });
}
}
⚠️ 注意事项与避坑指南
- 绝对不要在 localStorage 中存储敏感 Token :很多开发者习惯用
localStorage.setItem('token', xxx),但这会让 Token 暴露在document.cookie或全局 JS 环境中,一旦发生 XSS 攻击,黑客可以轻易盗取你的 Token。 - CSRF 防护不能少 :虽然
SameSite属性已经能抵御大部分 CSRF 攻击,但在涉及修改数据的关键操作(如转账、改密)时,建议后端依然配合传统的 CSRF Token 机制进行双重校验。 - 退出登录的实现 :由于前端无法删除 HttpOnly Cookie,退出登录必须由前端调用后端的登出接口,后端再通过
Set-Cookie将该 Cookie 清空(通常是将maxAge设为 0 或赋一个空值)。