.NET Core 6验证Google签发JWT持续返回401:validatedToken赋值合理性探讨
我正在给我的ASP.NET Core WebApi(.NET 6)配置Google签发的JWT身份验证,在Program.cs里加了自定义的GoogleTokenValidator,配置代码如下:
builder.Services.AddAuthentication(o => { o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.IncludeErrorDetails = true; o.SecurityTokenValidators.Clear(); o.SecurityTokenValidators.Add(new GoogleTokenValidator()); o.SaveToken = true; });
验证器的核心代码是这样的:
using Google.Apis.Auth; public class GoogleTokenValidator : ISecurityTokenValidator { // ...其他接口实现代码省略 public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) { var payload = GoogleJsonWebSignature.ValidateAsync(securityToken, new GoogleJsonWebSignature.ValidationSettings()).Result; var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, payload.Name), new Claim(ClaimTypes.Name, payload.Name), new Claim(JwtRegisteredClaimNames.FamilyName, payload.FamilyName), new Claim(JwtRegisteredClaimNames.GivenName, payload.GivenName), new Claim(JwtRegisteredClaimNames.Email, payload.Email), new Claim(JwtRegisteredClaimNames.Sub, payload.Subject), new Claim(JwtRegisteredClaimNames.Iss, payload.Issuer), }; var principal = new ClaimsPrincipal(); principal.AddIdentity(new ClaimsIdentity(claims, "Google")); // 原本这里是赋值为null,会导致401错误,现在改成了下面这行 validatedToken = new JwtSecurityToken(securityToken); // validatedToken = null; // 原代码 return principal; } }
原本把validatedToken赋值为null时,JwtBearerHandler的HandleAuthenticateAsync方法里会执行tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);,这会直接返回401错误。现在我改成validatedToken = new JwtSecurityToken(securityToken)是否正确?另外为什么原解答里赋值为null还能正常工作呢?
附完整的Program.cs代码:
using Core.API.Infrastructure.OAuth; using Core.API.Infrastructure.WebApplicationBuilderServices; using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Runtime.ExceptionServices; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddAuthentication(authenticationOptions => { authenticationOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; authenticationOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; authenticationOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(jwtBearerOptions => { jwtBearerOptions.IncludeErrorDetails = true; jwtBearerOptions.SecurityTokenValidators.Clear(); jwtBearerOptions.SecurityTokenValidators.Add(new GoogleTokenValidator()); jwtBearerOptions.SaveToken = true; }); builder.Services.AddSwagger(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
解答
1. 你修改的validatedToken = new JwtSecurityToken(securityToken)是完全正确的
原因很直接:.NET 6里的JwtBearerHandler在验证通过后,会依赖validatedToken的属性(比如ValidTo)来设置认证上下文的过期时间——就是你提到的那行tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);。如果validatedToken是null,这里会直接抛出空引用异常,最终被框架捕获并返回401未授权错误。
你用原始JWT字符串创建JwtSecurityToken实例的做法很合理:这个对象会自动解析令牌里的所有元数据(包括过期时间、签发时间等),框架就能正常读取这些值完成后续流程,不会再触发空引用问题。
2. 原解答中赋值为null能正常工作的可能原因
我猜原解答的场景和你的存在差异,大概率是这几种情况:
- 使用的.NET版本不同:在.NET 5及更早的版本中,
JwtBearerHandler可能没有这段读取validatedToken.ValidTo的逻辑,或者对null值做了兼容处理,不会因为validatedToken为null就直接返回401。 - 未启用
SaveToken选项:你在配置里设置了o.SaveToken = true;,而原解答可能没开这个开关。当SaveToken为false时,框架可能不会去读取validatedToken的过期时间,所以null不会触发错误。 - 原代码被简化省略:可能原作者的实际代码里有手动设置
tokenValidatedContext.Properties.ExpiresUtc的逻辑,但写解答时只保留了核心验证部分,导致你直接抄过来会出问题。
额外的优化建议
其实你可以让代码更严谨一点:创建JwtSecurityToken时加上解析失败的处理,避免意外报错:
if (!JwtSecurityToken.TryParse(securityToken, out var parsedToken)) { throw new SecurityTokenInvalidException("Failed to parse Google JWT token"); } validatedToken = parsedToken;
另外,用.Result同步调用异步方法GoogleJsonWebSignature.ValidateAsync可能会导致死锁,建议改成异步实现(需要实现ISecurityTokenValidator的ValidateTokenAsync方法):
public async Task<ClaimsPrincipal> ValidateTokenAsync(string securityToken, TokenValidationParameters validationParameters, CancellationToken cancellationToken) { var payload = await GoogleJsonWebSignature.ValidateAsync(securityToken, new GoogleJsonWebSignature.ValidationSettings(), cancellationToken); var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, payload.Name), new Claim(ClaimTypes.Name, payload.Name), new Claim(JwtRegisteredClaimNames.FamilyName, payload.FamilyName), new Claim(JwtRegisteredClaimNames.GivenName, payload.GivenName), new Claim(JwtRegisteredClaimNames.Email, payload.Email), new Claim(JwtRegisteredClaimNames.Sub, payload.Subject), new Claim(JwtRegisteredClaimNames.Iss, payload.Issuer), }; var principal = new ClaimsPrincipal(); principal.AddIdentity(new ClaimsIdentity(claims, "Google")); if (!JwtSecurityToken.TryParse(securityToken, out var parsedToken)) { throw new SecurityTokenInvalidException("Failed to parse Google JWT token"); } validatedToken = parsedToken; return principal; }
这样既避免了同步调用异步方法的风险,也更符合ASP.NET Core的异步编程规范。
内容的提问来源于stack exchange,提问作者Francesco B.

