和风天气api密钥生成使用 ed25519(EdDSA) 签名算法签名 jwt。但是 .net 官方没有提供 ed25519 签名算法,所以要自行拼接 jwt 结构并使用第三方库进行签名
openssl 生成密钥对
macos 上可能要安装 openssl@3, brew install openssl@3
/opt/homebrew/opt/openssl@3/bin/openssl genpkey -algorithm ED25519 -out qweather-ed25519-private.pem # 生成私钥
/opt/homebrew/opt/openssl@3/bin/openssl pkey -pubout -in qweather-ed25519-private.pem > qweather-ed25519-public.pem # 生成公钥
和风天气上创建项目
得到 kid 与 sub
创建待签名字符串
jwt 结构 headerBase64.payloadBase64.signatureBase64, 先生成待签名部分 headerBase64.payloadBase64
生成 headerBase64
header 比较简单,直接拼接 json 字符串
Dictionary<string, object> headerDict = new() { { "alg", "EdDSA" }, { "kid", kid }, };
string headerJson = JsonSerializer.Serialize(headerDict);
string headerBase64 = Base64UrlEncoder.Encode(headerJson);
生成 payloadBase64
payload 部分比较复杂,以后可能还要扩展,更改,所以使用 Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler 生成一个 jwt , 然后取出中间的 payload 部分
SecurityTokenDescriptor descriptor = new() {
Subject = new ClaimsIdentity([new Claim("sub", sub), ]),
Expires = DateTime.UtcNow.AddHours(20),
IssuedAt = DateTime.UtcNow.AddSeconds(-60),
NotBefore = null,
SigningCredentials = null,
};
JsonWebTokenHandler handler = new();
handler.SetDefaultTimesOnTokenCreation = false; // 为了不自动生成 nbf, 和风建议不要生成
string ? token = handler.CreateToken(descriptor);
if (string.IsNullOrWhiteSpace(token)) {
throw new InvalidOperationException("无法创建 jwt token");
}
string payloadBase64 = token.Split('.')[1];
生成待签名字符串
headerBase64.payloadBase64
生成签名 jwt
添加 NuGet 包 BouncyCastle.Cryptography
从 pem 文件中载入 ed25519 private key
# 从 pem 文件中载入二进制格式的 key
byte[] keyData = await LoadPrivateKeyDataAsync(cancellationToken);
using MemoryStream stream = new(keyData);
using StreamReader reader = new(stream);
PemReader pemReader = new(reader);
object ? keyPair = pemReader.ReadObject();
return keyPair
switch {
AsymmetricKeyParameter and Ed25519PrivateKeyParameters privateKey => privateKey,
AsymmetricCipherKeyPair {
Private: Ed25519PrivateKeyParameters privateKey,
} => privateKey,
_ =>
throw new ArgumentException("无法加载私钥"),
};
签名
# 使用前面得到的 headerBase64 和 payloadBase64 拼接待签名字符串
string signingInput = $ "{headerBase64}.{payloadBase64}";
# 从 pem 文件中中加载私钥
Ed25519PrivateKeyParameters privateKey = await LoadPrivateKeyAsync(cancellationToken);
Ed25519Signer signer = new();
signer.Init(true, privateKey);
byte[] message = Encoding.UTF8.GetBytes(signingInput);
signer.BlockUpdate(message, 0, message.Length);
byte[] signatureBytes = signer.GenerateSignature();
string signatureBase64 = Base64UrlEncoder.Encode(signatureBytes);
# 得到最終結果, 在 jwt.io 上檢查, 和风后台也有检测 key 工具
return $ "{signingInput}.{signatureBase64}";