如何限制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
相关产品推荐
相关产品推荐

