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

使用带过期时间的Cookie实现Remember Me功能的问题排查

Cookie版Remember Me功能问题排查与修正

问题描述

尝试实现带过期时间的Cookie版Remember Me功能未果,目前可通过LocalStorage结合JWT正常实现,但希望改用Cookie+JWT方案并支持过期时间,怀疑登录逻辑存在问题,寻求问题定位与解决。

相关代码

AuthorizeController.cs

[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
  ApplicationUser user = await this.SignInManager.UserManager.FindByEmailAsync(login.Email);

  if (user == null)
  {
    List<string> errors = new List<string>();
    errors.Add("No such user has been found.");
    return BadRequest(new LoginResult
    {
      Successful = false,
      Errors = errors,
    });
  }

  bool emailConfirmed = await this.UserManager.IsEmailConfirmedAsync(user);

  if (!emailConfirmed)
  {
    List<string> errors = new List<string>();
    errors.Add("Email not confirmed.");
    return BadRequest(new LoginResult
    {
      Successful = false,
      Errors = errors,
    });
  }

  Microsoft.AspNetCore.Identity.SignInResult result =
    await this.SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, false);

  if (!result.Succeeded)
  {
    List<string> errors = new List<string>();
    errors.Add("Email and password are invalid.");
    return BadRequest(new LoginResult
    {
      Successful = false,
      Errors = errors,
    });
  }

  IList<string> roles = await this.SignInManager.UserManager.GetRolesAsync(user);

  List<Claim> claims = new List<Claim>
  {
    new Claim(ClaimTypes.Name, login.Email)
  };

  ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
  ClaimsPrincipal principal = new ClaimsPrincipal(identity);
  AuthenticationProperties props = new AuthenticationProperties
  {
    IsPersistent = true,
    ExpiresUtc = DateTime.UtcNow.AddMonths(1)
  };

  // to register the cookie to the browser
  this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();

  foreach (string role in roles)
  {
    claims.Add(new Claim(ClaimTypes.Role, role));
  }

  SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["JwtSecurityKey"]));
  SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
  DateTime expiry = DateTime.Now.AddDays(Convert.ToInt32(this.Configuration["JwtExpiryInDays"]));

  JwtSecurityToken token = new JwtSecurityToken(
    this.Configuration["JwtIssuer"],
    this.Configuration["JwtAudience"],
    claims,
    expires: expiry,
    signingCredentials: creds
  );

  return Ok(new LoginResult
  {
    Successful = true,
    Token = new JwtSecurityTokenHandler().WriteToken(token),
  });
}

Startup.cs

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
    options.TokenValidationParameters = new TokenValidationParameters
    {
      ValidateIssuer = true,
      ValidateAudience = true,
      ValidateLifetime = true,
      ValidateIssuerSigningKey = true,
      ValidIssuer = Configuration["JwtIssuer"],
      ValidAudience = Configuration["JwtAudience"],
      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
    };
  })
  .AddCookie(options =>
   {
     options.Cookie.Name = "MySpecialCookie";
     options.LoginPath = "/login";
     options.ExpireTimeSpan = TimeSpan.FromDays(30);
     options.SlidingExpiration = true;
     options.EventsType = typeof(CookieAuthEvent);
   });
services.AddScoped<CookieAuthEvent>();

services.AddAuthorization(config =>
{
  config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
  config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});

services.ConfigureApplicationCookie(options =>
{
  options.Cookie.HttpOnly = true;
  options.Events.OnRedirectToLogin = context =>
  {
    context.Response.StatusCode = 401;
    return Task.CompletedTask;
  };
});

AuthorizeApi.cs

public async Task<LoginResult> Login(LoginModel loginModel)
{
  HttpResponseMessage responseMessage = await this.HttpClient.PostAsJsonAsync("Authorize/Login", loginModel);
  LoginResult result = await responseMessage.Content.ReadFromJsonAsync<LoginResult>();

  if (result.Successful)
  {
    if (loginModel.RememberMe)
    {
      await this.LocalStorage.SetItemAsync("MySpecialToken", result.Token);
    }

    ((ApiAuthenticationStateProvider)this.AuthenticationStateProvider).MarkUserAsAuthenticated(result.Token);
    this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);

    return result;
  }

  return result;
}

ApiAuthenticationStateProvider.cs

public void MarkUserAsAuthenticated(string token)
{
  ClaimsPrincipal authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
  Task<AuthenticationState> authState = Task.FromResult(new AuthenticationState(authenticatedUser));
  NotifyAuthenticationStateChanged(authState);
}

问题点与修正方案

1. 默认认证方案冲突

当前默认认证方案是JwtBearerDefaults.AuthenticationScheme,但需要Cookie认证作为主要方案(或让授权逻辑支持两种方案)。修改Startup中的认证配置,将默认方案改为Cookie:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddJwtBearer(...) // 保留JWT配置
  .AddCookie(...);

2. SignInAsync异步调用错误

this.HttpContext.SignInAsync(...).Wait()会导致死锁,应替换为await:

await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props);

3. Claims添加顺序错误

在创建ClaimsIdentity之后才添加角色Claims,导致Cookie中的身份不包含角色信息。需先添加所有Claims再创建Identity:

List<Claim> claims = new List<Claim>
{
  new Claim(ClaimTypes.Name, login.Email)
};
// 先添加角色Claims
foreach (string role in roles)
{
  claims.Add(new Claim(ClaimTypes.Role, role));
}
// 再创建Identity和Principal
ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);

4. 客户端未利用Cookie

当前客户端仍使用LocalStorage存储JWT并手动添加Bearer头,未让浏览器自动携带Cookie。需修改客户端配置:

  • 启用HttpClient自动携带Cookie(跨域场景需额外配置WithCredentials):
// Blazor WebAssembly中在Program.cs配置HttpClient
builder.Services.AddScoped(sp => new HttpClient 
{ 
  BaseAddress = new Uri(builder.HostEnvironment.BaseAddress), 
  DefaultRequestVersion = HttpVersion.Version20 
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { UseCookies = true });
  • 调整ApiAuthenticationStateProvider,支持从Cookie解析身份信息。

5. ConfigureApplicationCookie覆盖问题

ConfigureApplicationCookie会覆盖AddCookie的部分配置,导致Cookie设置不一致。建议统一在AddCookie中配置所有Cookie选项,移除ConfigureApplicationCookie:

.AddCookie(options =>
{
  options.Cookie.Name = "MySpecialCookie";
  options.LoginPath = "/login";
  options.ExpireTimeSpan = TimeSpan.FromDays(30);
  options.SlidingExpiration = true;
  options.EventsType = typeof(CookieAuthEvent);
  options.Cookie.HttpOnly = true;
  options.Events.OnRedirectToLogin = context =>
  {
    context.Response.StatusCode = 401;
    return Task.CompletedTask;
  };
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 19:10:17