从零构建Ed25519密钥对实现和风天气JWT认证全流程指南
1. 为什么选择JWT认证替代传统API Key
在当今的API安全领域,JSON Web Token(JWT)正逐渐成为身份认证的主流方案。与传统的API Key相比,JWT提供了更高级别的安全性保障,特别适合需要精细控制访问权限的场景。
传统API Key的主要问题在于:
- 长期有效:一旦泄露,攻击者可以无限期使用
- 权限单一:无法区分不同操作或数据范围的权限
- 缺乏时效性:无法自动失效,必须手动撤销
JWT认证的核心优势体现在:
- 时效控制:通过exp字段设置精确的过期时间
- 数字签名:使用Ed25519算法确保令牌无法伪造
- 最小权限:可针对不同场景生成具有特定权限的令牌
# 传统API Key调用示例(不推荐) curl -H "X-QW-Api-Key: ABCD1234EFGH" \ 'https://api.qweather.com/v7/weather/now?location=101010100' # JWT认证调用示例(推荐) curl -H "Authorization: Bearer eyJhbGciOiJFZERTQS..." \ 'https://api.qweather.com/v7/weather/now?location=101010100'2. Ed25519密钥对的生成与管理
Ed25519是目前最先进的椭圆曲线数字签名算法之一,相比传统RSA具有以下优势:
| 特性 | Ed25519 | RSA-2048 |
|---|---|---|
| 签名速度 | 快3-4倍 | 基准值 |
| 验证速度 | 快5-6倍 | 基准值 |
| 密钥长度 | 256位 | 2048位 |
| 安全性 | 128位 | 112位 |
生成密钥对的具体步骤:
确保系统已安装OpenSSL 3.0+版本
# 检查OpenSSL版本 openssl version生成Ed25519私钥
openssl genpkey -algorithm ED25519 -out private.pem从私钥导出公钥
openssl pkey -pubout -in private.pem -out public.pem
重要提示:私钥文件(private.pem)必须严格保密,建议设置400权限并存储在安全位置。公钥文件(public.pem)需要上传到和风天气控制台。
常见问题排查:
- 如果遇到"Algorithm ED25519 not found"错误,说明OpenSSL版本过低
- Windows用户可通过winget安装最新版:
winget install OpenSSL.OpenSSL
3. 和风天气控制台配置
完成密钥生成后,需要将公钥配置到和风天气控制台:
- 登录控制台进入"项目管理"
- 选择目标项目点击"添加凭据"
- 选择"JSON Web Token"认证方式
- 复制public.pem的全部内容到公钥文本框
- 设置有意义的凭据名称(如"生产环境API")
配置完成后,系统会生成一个凭据ID(kid),这个ID需要记录并在后续生成JWT时使用。
验证公钥是否匹配的方法:
# 计算本地公钥SHA256 openssl dgst -sha256 public.pem # 与控制台显示的SHA256值比对4. JWT的组成与生成原理
一个标准的JWT由三部分组成,通过点号(.)连接:header.payload.signature
4.1 Header部分
必须包含算法类型和凭据ID:
{ "alg": "EdDSA", "kid": "T8GYP76CD2" }4.2 Payload部分
核心字段包括:
sub: 项目ID(控制台获取)iat: 签发时间(建议设置为当前时间-30秒)exp: 过期时间(最长不超过24小时)
示例Payload:
{ "sub": "29KVA6G27T", "iat": 1703912400, "exp": 1703916000 }4.3 Signature生成
签名是JWT安全性的核心,生成步骤:
- 对Header和Payload分别进行Base64URL编码
- 用点号连接两个编码结果
- 使用Ed25519私钥对连接后的字符串签名
- 对签名结果进行Base64URL编码
Base64URL与标准Base64的区别:替换
+/为-_并去掉末尾的=
5. 多语言实现示例
Python实现
import jwt import time private_key = """-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIMt6oO4GC+QnzZFEp/Q245fpquD+j5wKApSJaY2MHFuJ -----END PRIVATE KEY-----""" headers = {"alg": "EdDSA", "kid": "T8GYP76CD2"} payload = { "sub": "29KVA6G27T", "iat": int(time.time()) - 30, "exp": int(time.time()) + 3600 } token = jwt.encode(payload, private_key, algorithm="EdDSA", headers=headers) print(f"JWT: {token}")Node.js实现
const { SignJWT, importPKCS8 } = require('jose'); async function generateJWT() { const privateKey = await importPKCS8( `-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIMt6oO4GC+QnzZFEp/Q245fpquD+j5wKApSJaY2MHFuJ -----END PRIVATE KEY-----`, 'EdDSA' ); const jwt = await new SignJWT({ sub: '29KVA6G27T' }) .setProtectedHeader({ alg: 'EdDSA', kid: 'T8GYP76CD2' }) .setIssuedAt(Math.floor(Date.now() / 1000) - 30) .setExpirationTime('1h') .sign(privateKey); console.log(`JWT: ${jwt}`); }Java实现
import java.security.*; import java.util.*; import io.jsonwebtoken.*; public class JwtGenerator { public static void main(String[] args) { String privateKey = "-----BEGIN PRIVATE KEY-----\n" + "MC4CAQAwBQYDK2VwBCIEIMt6oO4GC+QnzZFEp/Q245fpquD+j5wKApSJaY2MHFuJ\n" + "-----END PRIVATE KEY-----"; long iat = System.currentTimeMillis() / 1000 - 30; long exp = iat + 3600; String jwt = Jwts.builder() .setHeaderParam("alg", "EdDSA") .setHeaderParam("kid", "T8GYP76CD2") .claim("sub", "29KVA6G27T") .claim("iat", iat) .claim("exp", exp) .signWith(Keys.hmacShaKeyFor(privateKey.getBytes())) .compact(); System.out.println("JWT: " + jwt); } }6. 实战:调用天气API完整流程
6.1 获取城市Location ID
import requests api_host = "https://api.qweather.com" endpoint = "/geo/v2/city/lookup" params = {"location": "北京"} response = requests.get(f"{api_host}{endpoint}", headers={"Authorization": f"Bearer {token}"}, params=params) location_id = response.json()["location"][0]["id"]6.2 查询实时天气
endpoint = "/v7/weather/now" params = { "location": location_id, "lang": "zh", "unit": "m" } response = requests.get(f"{api_host}{endpoint}", headers={"Authorization": f"Bearer {token}"}, params=params) weather_data = response.json() print(f"当前温度: {weather_data['now']['temp']}°C")6.3 处理API响应
典型成功响应:
{ "code": "200", "updateTime": "2025-07-17T17:18+08:00", "now": { "temp": "35", "feelsLike": "36", "text": "晴", "windDir": "东北风", "windScale": "2" } }常见错误代码:
401: JWT认证失败(检查过期时间/kid/sub)404: 接口路径错误(检查API Host和endpoint)429: 请求频率超限(检查调用频率)
7. 高级技巧与最佳实践
JWT缓存策略:
- 服务端应用:缓存时间设为exp-iat的80%
- 客户端应用:每次请求生成新JWT
密钥轮换方案:
- 生成新密钥对(new_private.pem, new_public.pem)
- 上传新公钥到控制台获得新kid
- 逐步将应用迁移到新kid
- 确认无旧JWT使用后删除旧凭据
性能优化建议:
# 使用会话保持连接池 session = requests.Session() session.headers.update({"Authorization": f"Bearer {token}"}) # 启用gzip压缩 session.headers.update({"Accept-Encoding": "gzip"})安全防护措施:
- 限制JWT有效期(前端建议15分钟,后端可设1小时)
- 监控异常JWT使用模式
- 定期轮换Ed25519密钥对(建议每3-6个月)
在实际项目中,我曾遇到时区问题导致JWT立即过期的情况。解决方案是在生成iat和exp时明确指定时区,确保服务器和客户端使用相同的时区标准。