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

.Net Identity Bearer认证:移动端与Web端令牌过期时间差异化配置问题

实现不同客户端的Bearer令牌差异化过期时间

你遇到的问题核心在于BearerTokenOptions是全局绑定到单个认证Scheme的配置,运行时修改无法影响令牌生成逻辑(因为生成逻辑在启动阶段就已绑定配置)。以下是两种可行的解决方案:

方案一:创建独立的Bearer认证Scheme

为Web端和移动端分别注册不同的Bearer Scheme,各自配置过期时间,登录API对应使用不同Scheme生成令牌。

1. 注册多Scheme配置

// Web端Bearer配置(1天过期)
builder.Services.AddOptions<BearerTokenOptions>("WebBearer")
    .Configure(options =>
    {
        options.BearerTokenExpiration = TimeSpan.FromDays(1);
    });

// 移动端Bearer配置(14天过期)
builder.Services.AddOptions<BearerTokenOptions>("MobileBearer")
    .Configure(options =>
    {
        options.BearerTokenExpiration = TimeSpan.FromDays(14);
    });

// 配置认证中间件,添加两个Scheme
builder.Services.AddAuthentication()
    .AddBearer("WebBearer", 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"]))
        };
    })
    .AddBearer("MobileBearer", options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // 与Web端保持一致的验证规则,确保令牌能被正确校验
            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"]))
        };
    });

2. 分客户端登录API生成对应令牌

Web端登录接口示例:

[ApiController]
[Route("api/web/login")]
public class WebLoginController : ControllerBase
{
    private readonly SignInManager<IdentityUser> _signInManager;
    private readonly IOptionsSnapshot<BearerTokenOptions> _optionsSnapshot;

    public WebLoginController(SignInManager<IdentityUser> signInManager, IOptionsSnapshot<BearerTokenOptions> optionsSnapshot)
    {
        _signInManager = signInManager;
        _optionsSnapshot = optionsSnapshot;
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginModel model)
    {
        var signInResult = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false);
        if (!signInResult.Succeeded) return Unauthorized();

        var user = await _signInManager.UserManager.FindByNameAsync(model.Username);
        // 获取Web端Scheme对应的配置
        var bearerOptions = _optionsSnapshot.Get("WebBearer");
        // 生成对应Scheme的令牌
        var token = await _signInManager.UserManager.GenerateUserTokenAsync(user, "WebBearer", IdentityConstants.TokenProvider);

        return Ok(new 
        { 
            AccessToken = token, 
            ExpiresIn = (int)bearerOptions.BearerTokenExpiration.TotalSeconds 
        });
    }
}

移动端登录接口只需将Scheme名称替换为"MobileBearer"即可。

方案二:动态生成令牌时指定过期时间

无需新增Scheme,在令牌生成阶段根据客户端类型动态设置过期时间,适合不需要区分Scheme的简单场景。

1. 自定义令牌生成Provider

public class DynamicExpirationTokenProvider : DataProtectorTokenProvider<IdentityUser>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IConfiguration _configuration;

    public DynamicExpirationTokenProvider(IDataProtectionProvider dataProtectionProvider,
        IOptions<DataProtectionTokenProviderOptions> options,
        IHttpContextAccessor httpContextAccessor,
        IConfiguration configuration) : base(dataProtectionProvider, options)
    {
        _httpContextAccessor = httpContextAccessor;
        _configuration = configuration;
    }

    public override async Task<string> GenerateAsync(string purpose, UserManager<IdentityUser> manager, IdentityUser user)
    {
        // 从请求头获取客户端类型(也可从请求参数获取)
        var clientType = _httpContextAccessor.HttpContext.Request.Headers["X-Client-Type"].ToString().ToLower();
        var expiration = clientType == "mobile" ? TimeSpan.FromDays(14) : TimeSpan.FromDays(1);

        // 手动生成JWT令牌并设置过期时间
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]);
        
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                new Claim(ClaimTypes.Name, user.UserName)
            }),
            Expires = DateTime.UtcNow.Add(expiration),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
            Issuer = _configuration["Jwt:Issuer"],
            Audience = _configuration["Jwt:Audience"]
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

2. 注册自定义Provider

builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddTokenProvider<DynamicExpirationTokenProvider>("DynamicExpiration");

3. 登录接口使用自定义Provider

[ApiController]
[Route("api/login")]
public class LoginController : ControllerBase
{
    private readonly SignInManager<IdentityUser> _signInManager;

    public LoginController(SignInManager<IdentityUser> signInManager)
    {
        _signInManager = signInManager;
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginModel model)
    {
        var signInResult = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false);
        if (!signInResult.Succeeded) return Unauthorized();

        var user = await _signInManager.UserManager.FindByNameAsync(model.Username);
        var token = await _signInManager.UserManager.GenerateUserTokenAsync(user, "DynamicExpiration", IdentityConstants.TokenProvider);

        return Ok(new { AccessToken = token });
    }
}

为什么原有方案不生效

BearerTokenOptions是启动阶段绑定到指定Scheme的全局配置,运行时修改CurrentValue无法改变已初始化的令牌生成逻辑(生成逻辑在服务注册时已绑定初始配置),且全局修改会导致所有客户端令牌过期时间混乱,因此这种方式不可行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 16:14:52