.NetCore 3.1验证Azure AD生成的V1 JWT令牌签名始终返回false求排查
Azure AD V1 JWT验证代码问题排查与修正
存在的核心错误点
- 冗余的签名校验逻辑:前半部分已经初始化了
TokenValidationParameters和OpenID配置管理器,但是自定义的SignatureValidator直接返回JWT对象,完全跳过了官方类库内置的签名校验逻辑;后半部分又重复请求公钥接口手动校验,两套逻辑完全割裂,且前面的配置没有实际生效。 - 异步方法错误使用:
configManager.GetConfigurationAsync调用了.Result(),在ASP.NET Core上下文中容易引发死锁,同时你已经通过配置管理器拿到了SigningKeys,再单独请求公钥接口属于重复操作,完全多余。 - Base64编码处理错误:JWT使用的是Base64Url编码,不是标准Base64,直接调用
Convert.FromBase64String解码header会因-/_字符、缺少padding等问题抛出异常,被你吞掉后直接返回false。 - 签名处理完全错误:你直接对signature字符串做
UTF8.GetBytes,正确操作应该是先将Base64Url格式的signature字符串解码为字节数组,再用于校验。 - 哈希校验逻辑错误:
rsa.VerifyHash接收的第一个参数是原始内容的哈希值,你直接传入header.payload的原始字节,没有提前计算SHA256哈希,校验必然失败。 - 变量与异常处理问题:
isValid未初始化就返回,且catch块吞掉了所有异常,运行中的报错完全无法感知,多数情况下返回的都是默认值false。 - 公钥端点不匹配:如果你的应用是单租户应用,使用通用端点拉取公钥可能和令牌签发的租户公钥不匹配,导致kid校验失败。
修正后的代码
优先推荐直接用官方JwtSecurityTokenHandler完成校验,不需要手动实现签名逻辑,稳定性和安全性更高:
public async Task<bool> ValidateAsync(string token) { try { ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>( this.tokenSettings.StsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever()); // 改用await调用异步方法,避免死锁 OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync(CancellationToken.None); TokenValidationParameters validationParameters = new TokenValidationParameters { ValidIssuer = this.tokenSettings.IssuerValidator, ValidAudience = this.tokenSettings.Audience, IssuerSigningKeys = config.SigningKeys, ValidateAudience = true, ValidateIssuer = true, ValidateLifetime = true, // 去掉错误的SignatureValidator自定义逻辑,使用默认签名校验 }; JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); // 校验通过返回true return validatedToken != null; } catch (Exception ex) { // 可在此处打印ex.Message排查具体校验失败原因,比如颁发者不匹配、受众错误、令牌过期等 return false; } }
如果确实需要手动校验签名,需要补充Base64Url转码、哈希计算的逻辑,同时不要遗漏颁发者、受众、有效期的校验:
// 补充Base64Url解码方法 private byte[] Base64UrlDecode(string input) { input = input.Replace('-', '+').Replace('_', '/'); switch (input.Length % 4) { case 2: input += "=="; break; case 3: input += "="; break; } return Convert.FromBase64String(input); } public async Task<bool> ValidateAsync(string token) { bool isValid = false; try { var allParts = token.Split("."); if (allParts.Length != 3) return false; string header = allParts[0]; string payload = allParts[1]; string signature = allParts[2]; // Base64Url解码header获取kid var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header)); var kid = JObject.Parse(headerJson)["kid"].ToString(); // 复用配置管理器拿到的SigningKeys,无需重复请求接口 ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>( this.tokenSettings.StsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever()); OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync(CancellationToken.None); var signingKey = config.SigningKeys.FirstOrDefault(k => k.KeyId == kid) as X509SecurityKey; if (signingKey == null) return false; byte[] bytesToSign = Encoding.UTF8.GetBytes(string.Join(".", header, payload)); // 先计算待校验内容的SHA256哈希 byte[] hashBytes = SHA256.Create().ComputeHash(bytesToSign); // Base64Url解码签名内容 byte[] signatureToCheck = Base64UrlDecode(signature); using (RSA rsa = signingKey.Certificate.GetRSAPublicKey()) { isValid = rsa.VerifyHash(hashBytes, signatureToCheck, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } // 签名校验通过后,补充校验颁发者、受众、过期时间等核心参数 if (isValid) { var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload)); var payloadObj = JObject.Parse(payloadJson); if (payloadObj["iss"].ToString() != this.tokenSettings.IssuerValidator || payloadObj["aud"].ToString() != this.tokenSettings.Audience || payloadObj["exp"].ToObject<long>() < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { isValid = false; } } } catch (Exception ex) { // 可在此处打印异常日志排查问题 isValid = false; } return isValid; }
内容的提问来源于stack exchange,提问作者Scorpy47
相关产品推荐
相关产品推荐

