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

如何限制ASP.NET Web API令牌跨会话/浏览器复用?

解决方案:限制ASP.NET Web API令牌仅在生成会话/浏览器中可用

你的问题核心在于普通令牌(如JWT)是无状态的,不绑定客户端会话环境,所以复制后能跨浏览器复用。之前尝试用Session ID无效,是因为Web API默认未启用Session,且Angular请求可能未携带Session Cookie,导致每次请求生成新的Session ID。以下是几种可行的解决方案:

方案1:将令牌绑定到客户端特征(User Agent + IP)

生成令牌时,把请求的客户端标识(User Agent和IP地址)哈希后存入令牌的Claim;每次API验证令牌时,对比当前请求的客户端标识与令牌中的Claim,不一致则拒绝请求。

代码示例

生成令牌时添加客户端指纹

// 获取客户端特征
var userAgent = Request.Headers["User-Agent"].ToString();
var clientIp = HttpContext.Connection.RemoteIpAddress.ToString();
// 生成唯一指纹(SHA256哈希)
var clientFingerprint = HashHelper.ComputeSha256Hash($"{userAgent}{clientIp}");

// 构建Claims
var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, username),
    new Claim("ClientFingerprint", clientFingerprint)
};

// 生成JWT令牌
var token = new JwtSecurityToken(
    issuer: _config["Jwt:Issuer"],
    audience: _config["Jwt:Audience"],
    claims: claims,
    expires: DateTime.Now.AddMinutes(30),
    signingCredentials: new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);

哈希工具类

public static class HashHelper
{
    public static string ComputeSha256Hash(string input)
    {
        using (var sha256 = SHA256.Create())
        {
            byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
            var builder = new StringBuilder();
            foreach (var b in bytes) builder.Append(b.ToString("x2"));
            return builder.ToString();
        }
    }
}

验证令牌时校验客户端指纹

可以在自定义AuthorizationFilter或中间件中实现:

public class ClientFingerprintValidationFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var userAgent = context.HttpContext.Request.Headers["User-Agent"].ToString();
        var clientIp = context.HttpContext.Connection.RemoteIpAddress.ToString();
        var currentFingerprint = HashHelper.ComputeSha256Hash($"{userAgent}{clientIp}");

        var fingerprintClaim = context.HttpContext.User.FindFirst("ClientFingerprint");
        if (fingerprintClaim == null || fingerprintClaim.Value != currentFingerprint)
        {
            context.Result = new UnauthorizedResult();
        }
    }
}

方案2:使用Refresh Token绑定会话(推荐生产环境)

短有效期的Access Token用于接口请求,长有效期的Refresh Token存储在服务器,同时关联客户端特征和用户会话。Access Token过期后,只有匹配客户端特征的Refresh Token才能获取新的Access Token,从根源上限制跨会话复用。

代码示例

登录时返回Access Token和Refresh Token

// 生成Access Token(同方案1,包含客户端指纹)
var accessToken = GenerateAccessToken(username, clientFingerprint);

// 生成Refresh Token并存储到数据库
var refreshToken = Guid.NewGuid().ToString();
_dbContext.RefreshTokens.Add(new RefreshToken
{
    Token = refreshToken,
    UserId = userId,
    ClientFingerprint = clientFingerprint,
    Expires = DateTime.Now.AddDays(7),
    IsUsed = false
});
_dbContext.SaveChanges();

return Ok(new
{
    AccessToken = accessToken,
    RefreshToken = refreshToken
});

刷新Access Token时验证客户端特征

[HttpPost("refresh-token")]
public IActionResult RefreshToken([FromForm] string refreshToken)
{
    var tokenRecord = _dbContext.RefreshTokens.FirstOrDefault(t => 
        t.Token == refreshToken && !t.IsUsed && t.Expires > DateTime.Now);
    
    if (tokenRecord == null) return Unauthorized();

    // 校验客户端特征
    var userAgent = Request.Headers["User-Agent"].ToString();
    var clientIp = HttpContext.Connection.RemoteIpAddress.ToString();
    var currentFingerprint = HashHelper.ComputeSha256Hash($"{userAgent}{clientIp}");
    
    if (tokenRecord.ClientFingerprint != currentFingerprint)
    {
        // 标记该Refresh Token为已使用,防止恶意复用
        tokenRecord.IsUsed = true;
        _dbContext.SaveChanges();
        return Unauthorized();
    }

    // 生成新的Access Token和Refresh Token
    var newAccessToken = GenerateAccessToken(tokenRecord.UserId, currentFingerprint);
    var newRefreshToken = Guid.NewGuid().ToString();
    
    tokenRecord.IsUsed = true;
    _dbContext.RefreshTokens.Add(new RefreshToken
    {
        Token = newRefreshToken,
        UserId = tokenRecord.UserId,
        ClientFingerprint = currentFingerprint,
        Expires = DateTime.Now.AddDays(7),
        IsUsed = false
    });
    _dbContext.SaveChanges();

    return Ok(new { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
}

方案3:启用Session并绑定令牌到Session ID

通过启用ASP.NET Session,将令牌与Session ID关联存储,每次请求验证令牌对应的Session ID与当前请求的Session ID是否一致。

配置Session和CORS

在Startup.cs中配置:

public void ConfigureServices(IServiceCollection services)
{
    // 启用Session
    services.AddSession(options =>
    {
        options.IdleTimeout = TimeSpan.FromMinutes(30);
        options.Cookie.HttpOnly = true;
        options.Cookie.IsEssential = true;
    });

    // 配置CORS允许携带Cookie
    services.AddCors(options =>
    {
        options.AddPolicy("AllowAngular",
            builder => builder.WithOrigins("http://localhost:4200")
                              .AllowAnyHeader()
                              .AllowAnyMethod()
                              .AllowCredentials());
    });

    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseCors("AllowAngular");
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseSession(); // 启用Session中间件
    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

登录时关联令牌与Session ID

var token = GenerateAccessToken(username);
var sessionId = HttpContext.Session.Id;

// 用分布式缓存(如Redis)存储关联关系,内存缓存仅适合单服务器场景
_cache.SetString($"Token_Session_{token}", sessionId, new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});

验证令牌时校验Session ID

[Authorize]
public IActionResult SomeProtectedAction()
{
    var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
    var storedSessionId = _cache.GetString($"Token_Session_{token}");

    if (storedSessionId == null || storedSessionId != HttpContext.Session.Id)
    {
        return Unauthorized();
    }

    // 业务逻辑...
    return Ok();
}

注意事项

  • Angular请求需设置withCredentials: true,确保携带Session Cookie:
    this.http.get('/api/protected', { withCredentials: true }).subscribe(...);
    

总结

  • 方案1实现简单,适合快速迭代场景,但客户端特征存在被伪造的可能;
  • 方案2安全性最高,是生产环境的推荐方案,需做好Refresh Token的存储和过期管理;
  • 方案3依赖Session,适合传统会话绑定场景,但需注意分布式部署下的Session共享问题(需用Redis等分布式缓存)。

内容的提问来源于stack exchange,提问作者user2807255

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 16:13:36