Asp.net Web Api中JWT认证下[Authorize]返回401错误求助
嘿,我完全懂这种卡了两天的挫败感——能成功拿到token和refresh token,但就是过不了[Authorize]的关卡,确实让人挠头。咱们对着你的代码一步步排查,找出问题所在:
1. 认证类型不匹配(最可能的原因)
在OAuthTokenProvider的GrantResourceOwnerCredentials方法里,你创建ClaimsIdentity时用的是自定义的"JWT"认证类型:
ClaimsIdentity oAuthIdentity = new ClaimsIdentity("JWT");
但JWT Bearer认证中间件默认只会识别**"Bearer"**类型的身份标识。这就导致中间件无法关联你的token和用户身份,直接返回401。
修复方案:
把认证类型改成OAuthDefaults.AuthenticationType(它的底层值就是"Bearer"),或者直接写"Bearer":
// 推荐使用框架常量,避免硬编码错误 ClaimsIdentity oAuthIdentity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
2. JWT签名与验证配置不一致
检查你的Config.AudienceId和Config.AudienceSecret在两个地方是否完全一致:
- 生成token的
JwtFormat.Protect方法 - 验证token的
ConfigureOAuthTokenConsumption方法
哪怕是大小写、多余空格或者Base64解码错误,都会导致中间件判定token无效。
验证技巧:
把生成的token复制到JWT解码工具里(本地工具就行),核对以下字段:
iss(签发者)是否等于你配置的http://localhost:49860aud(受众)是否等于你的Config.AudienceId- 签名是否显示为“有效”
3. ClaimsIdentity缺少必要声明
你的ClaimsIdentity里只设置了认证类型,没有添加任何用户相关的声明(比如用户名、角色)。虽然理论上不是强制要求,但部分场景下中间件会因为身份标识没有可验证的信息而拒绝授权。
修复方案:
添加几个基础声明:
ClaimsIdentity oAuthIdentity = new ClaimsIdentity(OAuthDefaults.AuthenticationType); // 添加用户名声明 oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); // 可选:添加角色声明,方便后续基于角色的授权 oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, "RegularUser"));
4. OWIN中间件顺序错误
你的Startup里先配置了token生成中间件,再配置token验证中间件:
ConfigureOAuthTokenGeneration(app); ConfigureOAuthTokenConsumption(app);
OWIN中间件是按添加顺序执行的,这意味着带有[Authorize]的请求会先经过授权服务器中间件(它只处理/oauth/token路径),然后才到JWT验证中间件——此时授权逻辑已经触发,但身份还没被验证,自然返回401。
修复方案:
调换配置顺序,先让验证中间件处理所有请求:
ConfigureOAuthTokenConsumption(app); ConfigureOAuthTokenGeneration(app);
5. (次要)RefreshTokenProvider的同步方法未实现
虽然这不是当前401的直接原因,但你的RefreshTokenProvider里同步的Create和Receive方法抛了NotImplementedException。如果后续有同步调用的场景,会导致错误。建议补充实现:
public void Create(AuthenticationTokenCreateContext context) { CreateAsync(context).Wait(); } public void Receive(AuthenticationTokenReceiveContext context) { ReceiveAsync(context).Wait(); }
附上你的实现代码(优化后版本)
Startup.cs
public class Startup { public void Configuration(IAppBuilder app) { // 修正顺序:先配置token验证,再配置token生成 ConfigureOAuthTokenConsumption(app); ConfigureOAuthTokenGeneration(app); } private void ConfigureOAuthTokenGeneration(IAppBuilder app) { OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() { AllowInsecureHttp = true, TokenEndpointPath = new PathString("/oauth/token"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), Provider = new OAuthTokenProvider(), RefreshTokenProvider = new RefreshTokenProvider(), AccessTokenFormat = new Provider.JwtFormat("http://localhost:49860") }; app.UseOAuthAuthorizationServer(OAuthServerOptions); } private void ConfigureOAuthTokenConsumption(IAppBuilder app) { var issuer = "http://localhost:49860"; string audienceId = Config.AudienceId; byte[] audienceSecret = TextEncodings.Base64Url.Decode(Config.AudienceSecret); app.UseJwtBearerAuthentication( new JwtBearerAuthenticationOptions { AuthenticationMode = AuthenticationMode.Active, AllowedAudiences = new[] { audienceId }, IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[] { new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret) } }); } }
OAuthTokenProvider.cs
public class OAuthTokenProvider : OAuthAuthorizationServerProvider { public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); return Task.FromResult<object>(null); } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { var allowedOrigin = "*"; context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin }); if (context.UserName != context.Password) { context.SetError("invalid_grant", "The user name or password is incorrect."); return; } var props = new AuthenticationProperties(new Dictionary<string, string> { { "as:client_id", "Kaushik Thanki" } }); // 修正认证类型 ClaimsIdentity oAuthIdentity = new ClaimsIdentity(OAuthDefaults.AuthenticationType); // 添加用户声明 oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); var ticket = new AuthenticationTicket(oAuthIdentity, props); context.Validated(ticket); } }
JwtFormat.cs
public class JwtFormat : ISecureDataFormat<AuthenticationTicket> { private readonly string _issuer = string.Empty; public JwtFormat(string issuer) { _issuer = issuer; } public string Protect(AuthenticationTicket data) { if (data == null) { throw new ArgumentNullException("data"); } string audienceId = Config.AudienceId; string symmetricKeyAsBase64 = Config.AudienceSecret; var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64); var issued = data.Properties.IssuedUtc; var expires = data.Properties.ExpiresUtc; var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime); var handler = new JwtSecurityTokenHandler(); var jwt = handler.WriteToken(token); return jwt; } public AuthenticationTicket Unprotect(string protectedText) { throw new NotImplementedException(); } }
RefreshTokenProvider.cs
public class RefreshTokenProvider : IAuthenticationTokenProvider { private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>(); // 补充同步实现 public void Create(AuthenticationTokenCreateContext context) { CreateAsync(context).Wait(); } public async Task CreateAsync(AuthenticationTokenCreateContext context) { var guid = Guid.NewGuid().ToString(); var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary) { IssuedUtc = context.Ticket.Properties.IssuedUtc, ExpiresUtc = DateTime.UtcNow.AddYears(1) }; var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties); _refreshTokens.TryAdd(guid, refreshTokenTicket); context.SetToken(guid); } // 补充同步实现 public void Receive(AuthenticationTokenReceiveContext context) { ReceiveAsync(context).Wait(); } public async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { AuthenticationTicket ticket; if (_refreshTokens.TryRemove(context.Token, out ticket)) { context.SetTicket(ticket); } } }
内容的提问来源于stack exchange,提问作者Kaushik Thanki

