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

ASP.NET Core Web API单设备登录的自定义授权过滤方案咨询

方案可行性说明

自定义授权过滤器的方案完全可行。核心思路是:在用户每次登录时生成唯一的会话标识(如SessionId)并存储,同时将该标识嵌入JWT Token的Claim中;自定义授权过滤器在每次API请求时,验证当前Token中的会话标识是否与存储的用户最新会话标识一致,不一致则判定为未授权(即用户已在其他设备登录)。

具体实现步骤与代码示例

1. 实现用户会话存储服务

用于记录每个用户当前有效的会话标识,支持Redis或数据库存储,这里以Redis为例:

public interface IUserSessionStore
{
    // 存储用户最新会话ID
    Task SetLatestSessionIdAsync(string userId, string sessionId);
    // 获取用户最新会话ID
    Task<string> GetLatestSessionIdAsync(string userId);
}

// Redis实现
public class RedisUserSessionStore : IUserSessionStore
{
    private readonly IDatabase _redisDb;
    private const string SessionKeyPrefix = "UserSession:";

    public RedisUserSessionStore(IConnectionMultiplexer redis)
    {
        _redisDb = redis.GetDatabase();
    }

    public async Task SetLatestSessionIdAsync(string userId, string sessionId)
    {
        var key = $"{SessionKeyPrefix}{userId}";
        // 有效期与Refresh Token保持一致
        await _redisDb.StringSetAsync(key, sessionId, TimeSpan.FromDays(7));
    }

    public async Task<string> GetLatestSessionIdAsync(string userId)
    {
        var key = $"{SessionKeyPrefix}{userId}";
        return await _redisDb.StringGetAsync(key);
    }
}

2. 自定义单设备授权过滤器

继承IAuthorizationFilter,在请求时验证会话标识有效性:

public class SingleDeviceAuthorizationFilter : IAuthorizationFilter
{
    private readonly IUserSessionStore _sessionStore;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public SingleDeviceAuthorizationFilter(IUserSessionStore sessionStore, IHttpContextAccessor httpContextAccessor)
    {
        _sessionStore = sessionStore;
        _httpContextAccessor = httpContextAccessor;
    }

    public async void OnAuthorization(AuthorizationFilterContext context)
    {
        // 从Token Claim中获取用户ID和当前会话ID
        var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var currentSessionId = _httpContextAccessor.HttpContext.User.FindFirstValue("SessionId");

        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(currentSessionId))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // 对比当前会话ID与存储的最新会话ID
        var latestSessionId = await _sessionStore.GetLatestSessionIdAsync(userId);
        if (latestSessionId != currentSessionId)
        {
            context.Result = new UnauthorizedObjectResult("您的账号已在其他设备登录");
        }
    }
}

3. 注册服务与配置JWT

在Program.cs中注册相关服务,并配置全局过滤器(或局部使用):

var builder = WebApplication.CreateBuilder(args);

// 注册Redis连接(需提前安装StackExchange.Redis包)
builder.Services.AddSingleton<IConnectionMultiplexer>(
    ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]));
builder.Services.AddScoped<IUserSessionStore, RedisUserSessionStore>();

// 注册自定义授权过滤器
builder.Services.AddScoped<SingleDeviceAuthorizationFilter>();

// 保留原有JWT配置
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

// 全局注册过滤器(仅需单设备验证的接口也可局部标注)
builder.Services.AddControllers(options =>
{
    options.Filters.Add<SingleDeviceAuthorizationFilter>();
});

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

4. 改造登录与刷新Token逻辑

登录时生成会话ID并存储,刷新Token时更新会话ID:

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    private readonly IUserSessionStore _sessionStore;
    private readonly IConfiguration _configuration;

    public AuthController(IUserSessionStore sessionStore, IConfiguration configuration)
    {
        _sessionStore = sessionStore;
        _configuration = configuration;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginModel model)
    {
        // 替换为实际用户验证逻辑
        var user = ValidateUser(model.Username, model.Password);
        if (user == null)
        {
            return Unauthorized("用户名或密码错误");
        }

        // 生成唯一会话ID
        var sessionId = Guid.NewGuid().ToString();
        // 存储用户最新会话ID
        await _sessionStore.SetLatestSessionIdAsync(user.Id.ToString(), sessionId);

        // 生成含SessionId的JWT Token
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Username),
            new Claim("SessionId", sessionId)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: creds);

        // 生成Refresh Token(需存储到数据库关联用户)
        var refreshToken = Guid.NewGuid().ToString();

        return Ok(new
        {
            AccessToken = new JwtSecurityTokenHandler().WriteToken(token),
            RefreshToken = refreshToken,
            ExpiresIn = 30 * 60
        });
    }

    [HttpPost("refresh-token")]
    public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenModel model)
    {
        // 替换为实际Refresh Token验证逻辑
        var refreshTokenRecord = GetRefreshTokenFromDb(model.RefreshToken);
        if (refreshTokenRecord == null || refreshTokenRecord.ExpiresAt < DateTime.Now)
        {
            return Unauthorized("Refresh Token无效");
        }

        var userId = refreshTokenRecord.UserId.ToString();
        // 生成新会话ID并更新存储
        var newSessionId = Guid.NewGuid().ToString();
        await _sessionStore.SetLatestSessionIdAsync(userId, newSessionId);

        // 生成新的Access Token
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, userId),
            new Claim(ClaimTypes.Name, refreshTokenRecord.Username),
            new Claim("SessionId", newSessionId)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var newToken = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: creds);

        // 生成新Refresh Token并更新数据库
        var newRefreshToken = Guid.NewGuid().ToString();
        UpdateRefreshTokenInDb(model.RefreshToken, newRefreshToken);

        return Ok(new
        {
            AccessToken = new JwtSecurityTokenHandler().WriteToken(newToken),
            RefreshToken = newRefreshToken,
            ExpiresIn = 30 * 60
        });
    }

    // 模拟业务方法,需替换为实际逻辑
    private User ValidateUser(string username, string password) => new User { Id = 1, Username = username };
    private RefreshTokenRecord GetRefreshTokenFromDb(string refreshToken) => new RefreshTokenRecord { UserId = 1, Username = "test", ExpiresAt = DateTime.Now.AddDays(7) };
    private void UpdateRefreshTokenInDb(string oldRefreshToken, string newRefreshToken) { }
}

// 模型类
public class LoginModel { public string Username { get; set; } public string Password { get; set; } }
public class RefreshTokenModel { public string RefreshToken { get; set; } }
public class User { public int Id { get; set; } public string Username { get; set; } }
public class RefreshTokenRecord { public int UserId { get; set; } public string Username { get; set; } public DateTime ExpiresAt { get; set; } }

注意事项

  • 存储选择:Redis适合高并发场景,小应用可改用EF Core数据库存储会话标识。
  • 局部过滤器:若无需全局验证,可在指定控制器/Action上标注[ServiceFilter(typeof(SingleDeviceAuthorizationFilter))]。
  • 注销逻辑:用户主动注销时,需删除对应会话标识记录,确保后续请求直接返回未授权。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 19:17:05