You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 01:54:24