.Net Core API基于OpenId Connect实现强制登出方案咨询
服务器端强制登出Token的解决方案(基于.NET Core 6 + OpenIddict)
问题背景
用户点击登出时仅在客户端删除Token,但多标签页场景下存在漏洞:一个标签页完成登出后,另一个标签页保留的Token仍能通过服务器验证并正常调用接口,需要实现服务器端强制使Token失效的机制。
环境说明
- .NET Core 6 API项目,使用OpenIddict实现登录授权
- 用户数据存储与身份验证依赖外部API,当前API仅作为包装层,已集成OpenID Connect,登录返回Token的功能正常
- 现有OpenIddict配置及登出代码如下:
Program.cs 相关配置
builder.Services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore() .UseDbContext<ApplicationDbContext>(); }) .AddServer(options => { // 启用令牌端点和登出端点 options.SetTokenEndpointUris("/Connect/Token").SetLogoutEndpointUris("/Connect/Logout"); options.AllowPasswordFlow() .AllowRefreshTokenFlow(); options.AcceptAnonymousClients(); options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); options.UseAspNetCore() .EnableTokenEndpointPassthrough() .EnableLogoutEndpointPassthrough() .DisableTransportSecurityRequirement(); });
现有登出方法
[HttpPost("~/Connect/Logout"), Produces("application/json")] public async Task<IActionResult> Logout() { return SignOut( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties { RedirectUri = "https://localhost:****/Connect/Token" }); }
解决方案
1. 启用Token撤销端点(推荐)
OpenIddict原生支持Token撤销机制,只需开启端点并实现处理逻辑即可:
步骤1:添加撤销端点配置
修改Program.cs中的OpenIddict Server配置,启用撤销端点:
options.SetRevocationEndpointUris("/Connect/Revoke"); // 启用撤销端点 options.UseAspNetCore() .EnableTokenEndpointPassthrough() .EnableLogoutEndpointPassthrough() .EnableRevocationEndpointPassthrough(); // 启用撤销端点透传
步骤2:实现撤销接口
添加撤销端点的处理方法,接收并处理Token撤销请求:
[HttpPost("~/Connect/Revoke"), Produces("application/json")] public async Task<IActionResult> Revoke() { var revocationRequest = await HttpContext.GetOpenIddictServerRequestAsync(); if (revocationRequest is null) { return BadRequest(new OpenIddictResponse { Error = OpenIddictConstants.Errors.InvalidRequest, ErrorDescription = "无效的撤销请求。" }); } // 调用OpenIddict内置服务撤销Token var result = await HttpContext.RequestServices.GetRequiredService<IOpenIddictTokenManager>() .RevokeAsync(revocationRequest.Token, revocationRequest.TokenTypeHint); return result switch { OpenIddictResult.Success => Ok(new OpenIddictResponse()), OpenIddictResult.InvalidToken => BadRequest(new OpenIddictResponse { Error = OpenIddictConstants.Errors.InvalidToken, ErrorDescription = "要撤销的Token无效或已过期。" }), _ => BadRequest(new OpenIddictResponse { Error = OpenIddictConstants.Errors.ServerError, ErrorDescription = "撤销Token时发生服务器错误。" }) }; }
步骤3:前端配合调用
用户点击登出时,除了删除客户端本地Token,还需调用/Connect/Revoke端点,传入当前的访问令牌和刷新令牌,确保服务器端标记Token为已撤销。
2. 实现Token黑名单机制
若需要强制登出用户所有设备,可维护Token黑名单:
步骤1:创建黑名单存储
在ApplicationDbContext中添加Token黑名单实体:
public DbSet<TokenBlacklist> TokenBlacklists { get; set; } public class TokenBlacklist { public int Id { get; set; } public string Token { get; set; } = string.Empty; public DateTime ExpiresAt { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; }
步骤2:添加验证中间件
创建中间件,在每次API请求时检查Token是否在黑名单中:
public class TokenBlacklistMiddleware { private readonly RequestDelegate _next; public TokenBlacklistMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ApplicationDbContext dbContext) { var authorizationHeader = context.Request.Headers.Authorization.FirstOrDefault(); if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith("Bearer ")) { var token = authorizationHeader["Bearer ".Length..]; var isBlacklisted = await dbContext.TokenBlacklists.AnyAsync(t => t.Token == token && t.ExpiresAt > DateTime.UtcNow); if (isBlacklisted) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; await context.Response.WriteAsync("Token已被撤销,请重新登录。"); return; } } await _next(context); } } // 在Program.cs中注册中间件 app.UseMiddleware<TokenBlacklistMiddleware>();
步骤3:登出时加入黑名单
修改Logout方法,将当前Token加入黑名单:
[HttpPost("~/Connect/Logout"), Produces("application/json")] public async Task<IActionResult> Logout([FromServices] ApplicationDbContext dbContext) { // 从请求头获取当前Token var authorizationHeader = Request.Headers.Authorization.FirstOrDefault(); if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith("Bearer ")) { var token = authorizationHeader["Bearer ".Length..]; // 解析Token获取过期时间 var handler = new JwtSecurityTokenHandler(); if (handler.CanReadToken(token)) { var jwtToken = handler.ReadJwtToken(token); var expiresAt = jwtToken.ValidTo; dbContext.TokenBlacklists.Add(new TokenBlacklist { Token = token, ExpiresAt = expiresAt }); await dbContext.SaveChangesAsync(); } } return SignOut( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties { RedirectUri = "https://localhost:****/Connect/Token" }); }
3. 缩短Token有效期(辅助措施)
缩短访问令牌的有效期,减少Token被滥用的窗口时间,在OpenIddict配置中添加:
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(15)); // 设置访问令牌有效期为15分钟 options.SetRefreshTokenLifetime(TimeSpan.FromDays(7)); // 刷新令牌有效期可适当延长
内容的提问来源于stack exchange,提问作者Basanta Matia
相关产品推荐
相关产品推荐

