早些年在做RPA项目时,就用到了JWT Token,当时它已经作为一门新的技术点。当时引入该项技术,也是传统 Session 认证已经有它明显的局限性。
传统Session认证的局限:
传统Session认证用户登录的场景大多这样处理:
1)服务器创建一个唯一的 session ID 并存储在服务器端(如内存、数据库或文件系统);
2)session ID 通过 cookie 返回给客户端;
3)后续请求中,客户端通过 cookie 发送 session ID;
4)服务器验证 session ID 并查找对应的 session 数据
这样的处理方式,确实在系统需要分布式拓展时就会遇到很多问题,如:分布式系统中,session 数据需要在多个服务器间共享,会增加架构复杂度;另外大规模应用中,大量 session 数据会占用服务器内存资源;还有如果 session 存储服务器故障,可能影响整个系统的认证功能;当然最熟悉的场景就是跨域:浏览器 cookie 遵循同源策略,不同域名之间无法共享 cookie;对于CORS的配置,都需要在服务端配置复杂的 CORS 头,其实增加安全风险,还有安全方面而言:基于 cookie 的认证是很容易受到 CSRF(跨站请求伪造)攻击。
当然随着移动互联网的到来,还存在这样的一个问题:移动应用兼容性太差,我们会遇到如下问题:原生应用不支持 cookie:移动应用(iOS/Android)没有浏览器 cookie 机制,需要额外实现;状态管理困难:原生应用与 Web 应用的状态管理方式不一致,增加开发复杂度;
当然安全性问题也是需要重点考虑的,攻击者可以通过诱使用户使用特定 session ID 登录,还有我们常说的会话劫持(Session Hijacking):如果 cookie 被窃取(如通过 XSS 攻击),攻击者可以冒充用户;再加上CSRF 防护复杂:需要额外实现 CSRF 令牌机制,增加开发难度。
然JWT的出现解决了以上描述的这些问题,先科普一下相关知识。主要目的是了解该技术与传统方式的不同。
一、什么是 JWT Token?
1、 起源与背景
JSON Web Token(JWT)是一种基于 JSON 的开放标准(RFC 7519),用于在网络应用间安全地传输声明(claims)。它由互联网工程任务组(IETF)在 2015 年提出,旨在解决传统 session 认证的局限性,特别是在跨域和移动应用场景下。
2、核心概念
JWT 本质上是一个字符串,由三部分组成:
- Header(头部):通常包含令牌类型(如 "JWT")和使用的签名算法(如 HMAC SHA256 或 RSA)
- Payload(负载):包含声明(claims),即关于实体(通常是用户)和其他数据的声明
- Signature(签名):用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证 JWT 的发送者身份
二、JWT 的工作原理
接下来,我们在此拓展开来,更加细化的描述一下传统认证和JWT的对比。传统Session的认证,我在开篇已经描述了一下,还是在这再次述出来,主要是为了对比:
1、传统认证与 JWT 的对比
传统的基于 session 的认证流程:
- 用户登录后,服务器创建 session 并存储在服务器端
- 返回 session ID 给客户端(通常通过 cookie)
- 后续请求中,客户端发送 session ID
- 服务器验证 session ID 并查找对应的 session 数据
这种方式的缺点:
- 服务器需要存储大量 session 数据,扩展性差
- 跨域请求时存在 cookie 限制
- 移动端对 cookie 支持不佳
JWT 认证流程:
- 用户登录后,服务器生成 JWT 并返回给客户端
- 客户端在后续请求中将 JWT 包含在请求头中(通常是 Authorization 字段)
- 服务器验证 JWT 的签名和有效性
- 直接从 JWT 中获取用户信息,无需查询数据库
为了更好的演示,我用python语法进行一下描述
2、JWT 的生成与验证
下面是一个使用 Python 和 PyJWT 库生成和上述描述的验证 JWT 的过程:
import jwt
from datetime import datetime, timedelta
# 生成JWT
def generate_token(user_id, secret_key, expires_in=3600):
# 设置过期时间
expiration = datetime.utcnow() + timedelta(seconds=expires_in)
# 构建payload
payload = {
'user_id': user_id,
'exp': expiration,
'iat': datetime.utcnow(),
'iss': 'your_application', # 发行人
'aud': 'your_client' # 受众
}
# 生成JWT
token = jwt.encode(
payload,
secret_key,
algorithm='HS256'
)
return token
# 验证JWT
def verify_token(token, secret_key):
try:
# 验证并解码JWT
payload = jwt.decode(
token,
secret_key,
algorithms=['HS256'],
audience='your_client',
issuer='your_application'
)
return payload
except jwt.ExpiredSignatureError:
print('Token已过期')
return None
except jwt.InvalidAudienceError:
print('受众无效')
return None
except jwt.InvalidIssuerError:
print('发行人无效')
return None
except jwt.InvalidTokenError:
print('无效的Token')
return None
# 使用示例
if __name__ == '__main__':
secret_key = 'your_super_secret_key_here'
user_id = 12345
# 生成Token
token = generate_token(user_id, secret_key)
print(f"生成的JWT: {token}")
# 验证Token
payload = verify_token(token, secret_key)
if payload:
print(f"验证成功,用户ID: {payload['user_id']}")
3、JWT 的结构解析
让我们分解一个实际的 JWT 示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NSwiZXhwIjoxNjkyMjY4ODAwLCJpYXQiOjE2OTIyNjUyMDAsImlzcyI6InlvdXJfYXBwbGljYXRpb24iLCJhdWQiOiJ5b3VyX2NsaWVudCJ9.-KX2h4p1dXJnJ2oK8eX8dK2bX5eK7o8zJ3dW5cQ7e8Y
这个 JWT 可以分为三部分:
-
Header(Base64 编码):
{ "alg": "HS256", "typ": "JWT" }
-
Payload(Base64 编码):
{ "user_id": 12345, "exp": 1692268800, "iat": 1692265200, "iss": "your_application", "aud": "your_client" }
-
Signature:
-KX2h4p1dXJnJ2oK8eX8dK2bX5eK7o8zJ3dW5cQ7e8Y
签名的计算方式:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
通过以上方式我们可以明显的发现JWT的优势:
三、JWT 的优势与适用场景
1、主要优势
- 无状态:JWT 包含了所有必要信息,服务器不需要存储 session 数据
- 跨域支持:可以通过 localStorage 或 cookie 存储,并在请求头中发送
- 安全性:使用签名验证,防止篡改;支持加密
- 扩展性:可以包含丰富的用户信息,减少数据库查询
- 多平台兼容:适用于 Web、移动应用和 API
2、适用场景
- 身份验证:用户登录后返回 JWT,后续请求中验证
- 信息交换:安全地在各方之间传输信息
- 单点登录(SSO):多个服务可以共享同一个 JWT
- 微服务架构:在不同服务之间传递用户身份和权限
接下来,我们就用这四个场景用传统session和jwt来对比一下:
四、具体场景对比
1、跨域 API 调用
-
传统 session:
- 需要在 API 服务器配置复杂的 CORS 头
- 客户端需要处理 cookie 的跨域问题
- 可能遇到浏览器的预检请求(Preflight Request)性能问题
-
JWT:
- 客户端直接在请求头中包含 JWT(如
Authorization: Bearer <token>
) - API 服务器只需验证 JWT 签名,无需处理 cookie
- 简化 CORS 配置,通常只需允许
Authorization
头
- 客户端直接在请求头中包含 JWT(如
2、微服务架构
-
传统 session:
- 多个服务之间需要共享 session 数据
- 需要统一的 session 存储服务(如 Redis 集群)
- 服务间调用需要传递 session 信息
-
JWT:
- 每个服务独立验证 JWT,无需共享 session
- 用户信息直接包含在 JWT 中,服务间调用无需额外查询
- 可以实现细粒度的权限控制(如在 JWT 中包含用户角色或权限列表)
3、移动应用认证
-
传统 session:
- 需要在移动应用中模拟 cookie 机制
- 处理 session 过期和刷新逻辑复杂
- 难以与 Web 应用保持一致的认证体验
-
JWT:
- 移动应用可以轻松存储和管理 JWT
- 与 Web 应用使用相同的认证流程
- 可以实现自动刷新机制(如使用刷新令牌)
五、JWT 在跨域和移动场景下的最佳实践
1、跨域安全
- 使用 HTTPS:确保 JWT 在传输过程中不被拦截
- 自定义请求头:使用
Authorization
头而非 cookie 存储 JWT - 验证 issuer 和 audience:在 JWT 验证时,确保令牌是发给当前服务的
- 限制 Token 作用域:为不同类型的请求颁发不同的 JWT(如访问令牌和刷新令牌)
2、 移动应用安全
- 安全存储:在移动设备上使用安全存储机制存储 JWT
- 生物识别增强:结合设备生物识别(如指纹、面部识别)增强安全性
- 令牌刷新机制:实现刷新令牌机制,避免频繁登录
- 设备绑定:在 JWT 中包含设备标识,检测异常登录
3、性能优化
- Token 压缩:避免在 JWT 中包含过多不必要的信息
- 缓存验证结果:对于频繁访问的资源,缓存 JWT 验证结果
- 使用短期令牌:减少令牌被盗用的风险,同时通过刷新令牌保持用户会话
下面结合我项目的经历,说说JWT的高级用法
六、JWT 的高级用法
1、 刷新 Token 机制
JWT 的一个缺点是一旦签发,直到过期前都有效,这可能带来安全风险。刷新 Token 机制可以解决这个问题:
import jwt
from datetime import datetime, timedelta
import secrets
# 生成访问Token和刷新Token
def generate_tokens(user_id, secret_key, refresh_secret, access_expires=3600, refresh_expires=86400):
# 生成访问Token
access_payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(seconds=access_expires),
'iat': datetime.utcnow(),
'type': 'access'
}
access_token = jwt.encode(access_payload, secret_key, algorithm='HS256')
# 生成刷新Token(使用不同的密钥和更长的过期时间)
refresh_payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(seconds=refresh_expires),
'iat': datetime.utcnow(),
'type': 'refresh',
'jti': secrets.token_hex(16) # 唯一标识符,用于撤销
}
refresh_token = jwt.encode(refresh_payload, refresh_secret, algorithm='HS256')
return {
'access_token': access_token,
'refresh_token': refresh_token
}
# 刷新访问Token
def refresh_access_token(refresh_token, secret_key, refresh_secret):
try:
# 验证刷新Token
refresh_payload = jwt.decode(
refresh_token,
refresh_secret,
algorithms=['HS256']
)
# 检查Token类型
if refresh_payload.get('type') != 'refresh':
raise jwt.InvalidTokenError('Invalid token type')
# 生成新的访问Token
access_payload = {
'user_id': refresh_payload['user_id'],
'exp': datetime.utcnow() + timedelta(seconds=3600),
'iat': datetime.utcnow(),
'type': 'access'
}
access_token = jwt.encode(access_payload, secret_key, algorithm='HS256')
return access_token
except jwt.ExpiredSignatureError:
print('刷新Token已过期,请重新登录')
return None
except jwt.InvalidTokenError:
print('无效的刷新Token')
return None
# 使用示例
if __name__ == '__main__':
secret_key = 'your_access_token_secret'
refresh_secret = 'your_refresh_token_secret'
user_id = 12345
# 生成Token对
tokens = generate_tokens(user_id, secret_key, refresh_secret)
print(f"访问Token: {tokens['access_token']}")
print(f"刷新Token: {tokens['refresh_token']}")
# 模拟刷新Token
new_access_token = refresh_access_token(tokens['refresh_token'], secret_key, refresh_secret)
if new_access_token:
print(f"刷新后的访问Token: {new_access_token}")
2、基于角色的访问控制(RBAC)
在 JWT 中包含用户角色信息,实现细粒度的访问控制:
# 生成包含角色信息的JWT
def generate_token_with_roles(user_id, roles, secret_key, expires_in=3600):
payload = {
'user_id': user_id,
'roles': roles, # 例如: ['admin', 'editor']
'exp': datetime.utcnow() + timedelta(seconds=expires_in),
'iat': datetime.utcnow()
}
return jwt.encode(payload, secret_key, algorithm='HS256')
# 验证并检查角色权限
def verify_and_check_role(token, secret_key, required_role):
try:
payload = jwt.decode(token, secret_key, algorithms=['HS256'])
# 检查用户是否有所需角色
if 'roles' in payload and required_role in payload['roles']:
return True
else:
return False
except jwt.ExpiredSignatureError:
return False
except jwt.InvalidTokenError:
return False
# Flask API中使用角色验证的示例
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = 'your_secret_key'
@app.route('/admin/dashboard')
def admin_dashboard():
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token missing'}), 401
token = token.replace('Bearer ', '')
# 检查用户是否有admin角色
if not verify_and_check_role(token, SECRET_KEY, 'admin'):
return jsonify({'message': 'Permission denied'}), 403
return jsonify({'message': 'Welcome to admin dashboard'})
3、多因素认证集成
将 JWT 与多因素认证(MFA)结合:
import pyotp
# 生成TOTP密钥和URL
def generate_mfa_secret(username):
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret)
provisioning_url = totp.provisioning_uri(
username,
issuer_name="Your Application"
)
return {
'secret': secret,
'qr_code_url': provisioning_url
}
# 验证MFA代码
def verify_mfa_code(secret, code):
totp = pyotp.TOTP(secret)
return totp.verify(code)
# 结合MFA的JWT生成
def generate_token_with_mfa(user_id, mfa_secret, mfa_code, secret_key):
# 验证MFA代码
if not verify_mfa_code(mfa_secret, mfa_code):
raise ValueError('Invalid MFA code')
# 生成包含MFA验证标记的JWT
payload = {
'user_id': user_id,
'mfa_verified': True,
'exp': datetime.utcnow() + timedelta(minutes=30),
'iat': datetime.utcnow()
}
return jwt.encode(payload, secret_key, algorithm='HS256')
4、与 OAuth 2.0 集成
JWT 可以作为 OAuth 2.0 的访问令牌(Access Token):
from authlib.integrations.flask_client import OAuth
# 配置OAuth客户端
oauth = OAuth()
# 注册OAuth提供者
oauth.register(
name='your_oauth_provider',
client_id='your_client_id',
client_secret='your_client_secret',
access_token_url='https://provider.com/oauth/token',
authorize_url='https://provider.com/oauth/authorize',
api_base_url='https://provider.com/api/',
client_kwargs={'scope': 'openid email profile'}
)
# 处理OAuth回调
@app.route('/auth/callback')
def callback():
# 获取访问令牌(可能是JWT)
token = oauth.your_oauth_provider.authorize_access_token()
# 如果返回的是JWT,可以直接解析
if 'id_token' in token:
# 验证并解析JWT
try:
id_token = jwt.decode(
token['id_token'],
verify=False, # 在实际应用中应该验证签名
algorithms=['RS256']
)
# 处理用户信息
user_id = id_token.get('sub')
email = id_token.get('email')
# 生成自己的应用JWT
app_token = generate_token(user_id, app.config['SECRET_KEY'])
return jsonify({
'message': 'Login successful',
'token': app_token,
'user': {
'id': user_id,
'email': email
}
})
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
else:
return jsonify({'message': 'No ID token received'}), 400
以下是我总结的JWT的安全实践的场景
六、JWT 的安全最佳实践
1、密钥管理
- 使用足够长且随机的密钥
- 对不同类型的 Token 使用不同的密钥(如访问 Token 和刷新 Token)
- 定期轮换密钥
- 不要在代码中硬编码密钥,使用环境变量或密钥管理服务
2、Token 存储
- 在浏览器中,使用 HttpOnly cookie 存储 JWT,防止 XSS 攻击
- 避免使用 localStorage 或 sessionStorage 存储 JWT
- 在移动应用中,使用安全的存储机制(如 iOS 的 Keychain 或 Android 的 Keystore)
3、防止 CSRF 攻击
- 如果使用 cookie 存储 JWT,确保设置 SameSite 属性
- 对于敏感操作,额外验证 CSRF 令牌
4、 其他安全措施
- 使用 HTTPS 确保 Token 在传输过程中的安全
- 设置合理的 Token 过期时间
- 实现 Token 撤销机制(如黑名单或刷新 Token)
- 对敏感信息进行加密,而不仅仅是签名
- 监控 Token 使用情况,检测异常活动
当然说了这么多,JWT也有其局限性,但这个局限性完全可以用其他方案组合解决问题。
七、JWT 的局限性与替代方案
1、主要局限性
- Token 体积:JWT 可能会比较大,特别是包含大量 claims 时
- 不可撤销性:一旦签发,直到过期前都有效(除非实现额外的撤销机制)
- 安全性依赖密钥:如果密钥泄露,整个系统将面临风险
- 性能开销:每次请求都需要验证签名,可能影响性能
2、替代方案
- OAuth 2.0:更适合授权场景,特别是第三方应用访问资源
- SAML:企业级单点登录解决方案
- Session Cookies:适合传统 Web 应用,不需要跨域支持
- PASETO:比 JWT 更安全的替代方案,解决了一些 JWT 的设计问题
最后小结
JWT 是一种强大的身份验证和信息交换机制,特别适合现代分布式系统和移动应用。它通过数字签名保证了数据的完整性和安全性,同时提供了良好的扩展性和跨平台支持。在使用 JWT 时,需要注意安全最佳实践,特别是密钥管理、Token 存储和防止常见的安全漏洞。对于复杂场景,可以结合刷新 Token、RBAC、MFA 等高级技术,进一步提升系统的安全性和用户体验。
尽管 JWT 有一些局限性,但在大多数场景下,它仍然是身份验证和授权的首选解决方案。随着技术的发展,我们也看到了一些改进 JWT 的新方案出现,如 PASETO,这些都值得我们进一步关注和探索。让我们拭目以待吧!