如何在对接Web API的ASP.NET Core MVC/Razor应用中优化JWT认证授权?
Great question! For your ASP.NET Core MVC/Razor Page app that relies on a backend API for JWT tokens, you absolutely don't need to manually handle token checks, refreshes, or sign-outs—ASP.NET Core's built-in authentication middleware can automate most of this, and it's far more maintainable than custom manual logic. Let's break down the optimal approach, plus how to implement custom auth mechanisms.
Instead of reinventing the wheel, leverage the framework's built-in tools to handle token lifecycle automatically. Here's how to set it up for cookie-stored JWTs:
1. Configure Cookie Authentication with Auto-Refresh Logic
First, register the cookie authentication service in Program.cs, and hook into its events to handle token expiration and refresh seamlessly:
builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(options => { // Secure cookie settings to block XSS/CSRF attacks options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; options.Events = new CookieAuthenticationEvents { // Validate token on every request OnValidatePrincipal = async context => { var accessToken = context.Properties.GetTokenValue("access_token"); var refreshToken = context.Properties.GetTokenValue("refresh_token"); if (string.IsNullOrEmpty(accessToken) || IsTokenExpired(accessToken)) { // Attempt to refresh the access token via your API var newTokens = await RefreshTokensAsync(refreshToken, context.HttpContext.RequestServices); if (newTokens != null) { // Update tokens stored in the cookie context.Properties.UpdateTokenValue("access_token", newTokens.AccessToken); context.Properties.UpdateTokenValue("refresh_token", newTokens.RefreshToken); // Extend the cookie session context.ShouldRenew = true; } else { // Refresh failed—sign out the user context.RejectPrincipal(); await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } } }, // Clean up tokens on sign-out OnSigningOut = async context => { var refreshToken = context.Properties.GetTokenValue("refresh_token"); // Call your API to revoke the refresh token (if supported) await RevokeRefreshTokenAsync(refreshToken, context.HttpContext.RequestServices); } }; });
Helper Methods for Token Handling
Add these utility functions to check token expiration and interact with your API:
private bool IsTokenExpired(string accessToken) { var handler = new JwtSecurityTokenHandler(); if (!handler.CanReadToken(accessToken)) return true; var jwtToken = handler.ReadJwtToken(accessToken); return jwtToken.ValidTo < DateTime.UtcNow; } private async Task<TokensDto> RefreshTokensAsync(string refreshToken, IServiceProvider services) { var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"); var response = await httpClient.PostAsync("/api/auth/refresh", JsonContent.Create(new { RefreshToken = refreshToken })); return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<TokensDto>() : null; } private async Task RevokeRefreshTokenAsync(string refreshToken, IServiceProvider services) { var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"); await httpClient.PostAsync("/api/auth/revoke", JsonContent.Create(new { RefreshToken = refreshToken })); }
2. Store Tokens in Cookie on Login
When your login call to the API succeeds, use SignInAsync to persist tokens in the cookie's authentication properties:
// After fetching tokens from your API var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.UserName), // Add other claims parsed from the JWT or API response new Claim(ClaimTypes.Role, user.Role) }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { Tokens = new Dictionary<string, string> { { "access_token", apiResponse.AccessToken }, { "refresh_token", apiResponse.RefreshToken } }, // Match cookie expiration to your refresh token's lifespan ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7) }; await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), authProperties);
If you need full control over auth flows, you can build custom components tailored to your needs:
Custom Authentication Scheme
Create a custom AuthenticationHandler to handle token retrieval, validation, and refresh entirely on your own:
public class CustomJwtCookieHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private readonly IHttpClientFactory _httpClientFactory; public CustomJwtCookieHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IHttpClientFactory httpClientFactory) : base(options, logger, encoder, clock) { _httpClientFactory = httpClientFactory; } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { // Get tokens directly from cookies var accessToken = Request.Cookies["access_token"]; var refreshToken = Request.Cookies["refresh_token"]; if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.NoResult(); // Validate access token via API if (!await IsAccessTokenValid(accessToken)) { // Refresh token if possible var newTokens = await RefreshTokensAsync(refreshToken); if (newTokens == null) return AuthenticateResult.Fail("Invalid or expired tokens"); // Update cookies with new tokens UpdateTokenCookies(newTokens); accessToken = newTokens.AccessToken; } // Create claims principal from JWT var jwtHandler = new JwtSecurityTokenHandler(); var jwtToken = jwtHandler.ReadJwtToken(accessToken); var claims = jwtToken.Claims.ToList(); var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); } private async Task<bool> IsAccessTokenValid(string accessToken) { var client = _httpClientFactory.CreateClient("ApiClient"); var response = await client.PostAsync("/api/auth/validate", JsonContent.Create(new { Token = accessToken })); return response.IsSuccessStatusCode; } private async Task<TokensDto> RefreshTokensAsync(string refreshToken) { var client = _httpClientFactory.CreateClient("ApiClient"); var response = await client.PostAsync("/api/auth/refresh", JsonContent.Create(new { RefreshToken = refreshToken })); return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<TokensDto>() : null; } private void UpdateTokenCookies(TokensDto tokens) { var cookieOptions = new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTime.UtcNow.AddDays(7) }; Response.Cookies.Append("access_token", tokens.AccessToken, cookieOptions); Response.Cookies.Append("refresh_token", tokens.RefreshToken, cookieOptions); } }
Register the custom scheme in Program.cs:
builder.Services.AddAuthentication() .AddScheme<AuthenticationSchemeOptions, CustomJwtCookieHandler>("CustomJwtCookie", options => { }); // Set as default authorization policy builder.Services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder("CustomJwtCookie") .RequireAuthenticatedUser() .Build(); });
Custom Authorization Policy
For granular access control, create a custom authorization requirement and handler:
// Custom requirement definition public class RequireAdminRoleRequirement : IAuthorizationRequirement { } // Handler to enforce the requirement public class RequireAdminRoleHandler : AuthorizationHandler<RequireAdminRoleRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireAdminRoleRequirement requirement) { if (context.User.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Admin")) { context.Succeed(requirement); } else { context.Fail(); } return Task.CompletedTask; } }
Register the handler and policy:
builder.Services.AddAuthorization(options => { options.AddPolicy("RequireAdmin", policy => policy.Requirements.Add(new RequireAdminRoleRequirement())); }); builder.Services.AddSingleton<IAuthorizationHandler, RequireAdminRoleHandler>();
Use the policy in controllers/Razor Pages:
[Authorize(Policy = "RequireAdmin")] public IActionResult AdminDashboard() { return View(); }
- Secure Cookies: Always use
HttpOnly,Secure, andSameSite=Strictfor token cookies to mitigate XSS and CSRF attacks. - Token Rotation: When refreshing tokens, issue a new refresh token (and invalidate the old one) to reduce the risk of token theft.
- Server-Side Token Revocation: Always call your API to revoke refresh tokens on sign-out to prevent unauthorized use.
内容的提问来源于stack exchange,提问作者Alyssa Green

