使用带过期时间的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
相关产品推荐
相关产品推荐

