ASP.NET Core Web API如何检测用户重登录后访问同一API
ASP.NET Core 6.0 微服务实现方案
1. 确定唯一匹配规则
不要使用SessionId、JWT的jti这类会话级标识做关联,这类值会在用户登出重登后刷新,无法匹配历史记录。必须用两个不随登录状态变化的稳定维度做联合唯一键:
- 用户维度:取系统内账号唯一不变的标识,比如用户表主键
UserId,直接从用户登录凭据的Claims中读取ClaimTypes.NameIdentifier对应的值即可,账号不注销该值永远不变 - API维度:对请求做归一化处理生成固定标识,优先取接口的路由模板+HTTP方法(比如
GET:api/v2/order/{id}),自动过滤动态路径参数、随机请求nonce、时间戳这类每次请求都会变化的内容;如果需要精确到同参数的请求匹配,可将业务参数做哈希后拼接在标识后。
2. 存储选型
微服务多实例部署场景下禁止使用本地内存存储,避免实例间数据不同步,二选一即可:
- 高性能场景选Redis等分布式缓存:Key按
err:hist:{userId}:{apiIdentifier}规则拼接,Value存储错误码、错误提示、最后触发时间即可,按需设置滑动过期时间(比如30天) - 需永久留存错误历史选关系型数据库:建
UserApiErrorHistory表,字段包含UserId、ApiIdentifier、ErrorContent、LastOccurTime,给(UserId, ApiIdentifier)建联合唯一索引,写入时直接做upsert覆盖同键的旧记录。
3. 核心逻辑埋点
只需要两个全局拦截点就能覆盖全流程,不用在每个业务接口单独写逻辑:
3.1 错误记录写入/更新/清除
用全局响应中间件统一处理:
- 当已登录用户的请求返回4xx/5xx错误时,排除401、403这类鉴权类错误,将当前用户、对应API的错误信息写入存储,覆盖同键的旧记录
- 当已登录用户的请求返回2xx成功时,直接删除存储中对应用户+对应API的错误记录,避免用户访问成功后下次进入还弹出历史错误
参考实现代码:
// 注册位置:放在异常处理中间件之后、路由中间件之前 app.Use(async (context, next) => { var originalBody = context.Response.Body; using var memStream = new MemoryStream(); context.Response.Body = memStream; await next(); // 只处理已登录用户的请求 if (context.User.Identity?.IsAuthenticated != true) { memStream.Seek(0, SeekOrigin.Begin); await memStream.CopyToAsync(originalBody); return; } var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); var endpoint = context.GetEndpoint(); var apiTemplate = endpoint?.Metadata.GetMetadata<RouteNameMetadata>()?.RouteName ?? context.Request.Path.Value?.ToLowerInvariant(); var apiKey = $"{context.Request.Method}:{apiTemplate}"; var cacheKey = $"err:hist:{userId}:{apiKey}"; // 请求成功,清除历史错误记录 if (context.Response.StatusCode is >= 200 and < 300) { await _distributedCache.RemoveAsync(cacheKey); memStream.Seek(0, SeekOrigin.Begin); await memStream.CopyToAsync(originalBody); return; } // 跳过鉴权类错误 if (context.Response.StatusCode is StatusCodes.Status401Unauthorized or StatusCodes.Status403Forbidden) { memStream.Seek(0, SeekOrigin.Begin); await memStream.CopyToAsync(originalBody); return; } // 写入新的错误记录 memStream.Seek(0, SeekOrigin.Begin); var errorContent = await new StreamReader(memStream).ReadToEndAsync(); await _distributedCache.SetStringAsync(cacheKey, errorContent, new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromDays(30) }); memStream.Seek(0, SeekOrigin.Begin); await memStream.CopyToAsync(originalBody); });
3.2 重登后访问接口的错误检查
用全局Action过滤器统一在请求进入业务逻辑前做检查:
- 已登录用户访问接口时,先查存储中是否存在对应用户+对应API的历史错误
- 若存在错误,按需选择处理方式:要么不阻断业务逻辑,将错误信息存在
HttpContext.Items中,随正常响应返回给前端做提示;要么直接返回历史错误,要求用户确认后携带ignorePreviousError=true参数跳过检查再正常访问接口
参考实现代码:
public class PreviousErrorCheckFilter : IAsyncActionFilter { private readonly IDistributedCache _cache; public PreviousErrorCheckFilter(IDistributedCache cache) { _cache = cache; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 携带忽略标记时跳过检查 if (context.HttpContext.Request.Query.ContainsKey("ignorePreviousError")) { await next(); return; } if (context.HttpContext.User.Identity?.IsAuthenticated == true) { var userId = context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); var endpoint = context.HttpContext.GetEndpoint(); var apiTemplate = endpoint?.Metadata.GetMetadata<RouteNameMetadata>()?.RouteName ?? context.HttpContext.Request.Path.Value?.ToLowerInvariant(); var apiKey = $"{context.HttpContext.Request.Method}:{apiTemplate}"; var cacheKey = $"err:hist:{userId}:{apiKey}"; var previousError = await _cache.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(previousError)) { // 方案1:不阻断请求,将错误存入上下文,后续响应时统一返回提示 context.HttpContext.Items["PreviousApiError"] = previousError; // 方案2:直接阻断返回错误,按需打开 // context.Result = new ContentResult // { // StatusCode = StatusCodes.Status400BadRequest, // Content = JsonSerializer.Serialize(new {msg = "您上次访问该接口触发错误:" + previousError, needConfirm = true}) // }; // return; } } await next(); } } // 全局注册过滤器 builder.Services.AddControllers(options => { options.Filters.Add<PreviousErrorCheckFilter>(); });
4. 边界注意事项
- 登出逻辑不需要做任何额外处理,因为存储的记录是和UserId绑定,和当前会话/JWT无关,登出销毁会话不会影响历史错误记录
- 错误内容存储前要做好敏感信息脱敏,不要把用户手机号、身份证号这类明文信息存在缓存或数据库中
- 如果接口做了版本控制,路由模板会自动携带版本号,不会出现不同版本接口错误匹配的问题
- 若使用分布式缓存,注意给Key设置合理的过期时间,避免冷数据长期占用内存
内容的提问来源于stack exchange,提问作者viren
相关产品推荐
相关产品推荐

