有状态Cookie

📅 2026/7/7 7:25:08 👁️ 阅读次数 📝 编程学习
有状态Cookie

相比上面无状态模式,有状态模式需要额外加一些代码用来存储读写和过期清理。如果是单服务实例,可以直接存入本地内存,但需注意服务重启的话会导致票据丢失,用户直接退出登录了。如果服务多实例负载均衡的情况下需要引入分布式存储来共享,把票据存入Redis或者Memcached中。如果你坚持本地内存可能会破坏负载均衡策略。

集成思路:

第一步:将认证方案注入容器

将认证方案,例如 Cookie 和它的配置注入容器,跟无状态一模一样的套路,就不贴代码了,参照无状态第1、2步。

第二步:实现 ITicketStore 接口

如果需要使用Cookie的有状态模式,框架的CookieAuthenticationOptions选项提供了SessionStore配置来进行实现,他是ITicketStore类型,这个接口是框架提供给用户扩展的,主要用于认证票据存在哪,我们需要实现它。

public class MemoryTicketStore(private readonly IMemoryCache _cache) : ITicketStore { private const string KeyPrefix = "auth-session:"; // 存 public Task StoreAsync(AuthenticationTicket ticket) { var key = $"{KeyPrefix}{Guid.NewGuid():N}"; RenewAsync(key, ticket); return Task.FromResult(key); } // 更新 public Task RenewAsync(string key, AuthenticationTicket ticket) { var expires = ticket.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.AddHours(8); _cache.Set(key, ticket, new MemoryCacheEntryOptions { AbsoluteExpiration = expires }); return Task.CompletedTask; } // 查询 public Task RetrieveAsync(string key) => Task.FromResult(_cache.Get(key)); // 删除 public Task RemoveAsync(string key) { _cache.Remove(key); return Task.CompletedTask; } }

你同样可以自己实现Redis存储的逻辑,只需替换MemoryTicketStoreRedisTicketStore,然后修改ITicketStore的注册,根本不需要改动身份验证配置。为了方便我使用的内存缓存,接下来需要将实现注入容器,再将它配置起来。

第三步:注入并配置
// 注入组件 builder.Services.AddMemoryCache(); builder.Services.AddSingleton<ITicketStore, MemoryTicketStore>(); // 配置实例延迟执行,防止依赖的服务没注册 builder.Services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme) .Configure<ITicketStore>((options, store) => options.SessionStore = store);
第四步:在控制器中使用

在控制器中使用和无状态模式一样使用。


2.3 使用 CookieAuthentication 登录实现

Cookie认证的方式登录,不需要我们实现生成Cookie,框架完全控制会话生命周期,SignInAsync()触发标准化的Cookie生成流程,登录代码只需要构建principal就行。

  1. 构建ClaimsPrincipal(含用户标识和权限声明)
  2. 调用SignInAsync()传递该主体
public async Task Login([FromBody] LoginRequest request) { if (request.Username != "admin" || request.Password != "123456") { return Unauthorized(new { message = "用户名或密码错误" }); } var claims = new[] { new Claim(ClaimTypes.Name, request.Username), new Claim(ClaimTypes.Role, "User") }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddHours(8) }); // 模式一:return Ok(new { message = "登录成功" }); return Ok(new { message = "登录成功,会话已保存在服务端" }); // 模式二(当前) }

3. 使用JWT的认证方式

3.1 集成JWT

接下来我们继续看看JWT认证的方式如何在NETCORE中集成和实现,其实jwt也是无状态的,通常服务器一旦颁发,在token过期之前后期是不可控的。

集成思路:

第一步:注册JWT服务

和集成Cookie认证一样,都需要先注册,然后启用认证中间件,而这里注册的是JWT相关的服务。

var authenticationBuilder = builder.Services.AddAuthentication(options => { // 将默认方案设置为JWT options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }); authenticationBuilder.AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, // 验证签发者,令牌中的 "iss" 字段必须与 ValidIssuer 完全匹配 ValidateAudience = true, // 是否验证受众,保证令牌是发给当前服务的 ValidateLifetime = true, // 是否验证令牌有效期 ValidateIssuerSigningKey = true, // 是否验证令牌签名 ValidIssuer = jwtIssuer, // 合法签发者标识 "https://鉴权服务.com" ValidAudience = jwtAudience, // 受众标识 // 令牌签名验证密钥,对称加密 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("JWTjiamimiyao!")) }; // JWT 认证流程中的事件钩子,未认证时触发 options.Events = new JwtBearerEvents { OnChallenge = context => { // 移除 WW-Authenticate 避免暴露敏感信息 context.HandleResponse(); context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.ContentType = "application/json"; return context.Response.WriteAsync(JsonSerializer.Serialize(new { error = "未认证", message = "Token无效或者过期." })); } }; });
第二步:在接口中使用

直接接口中使用[Authorize(AuthenticationSchemes = "Bearer")]就代表这一组接口都会启用jwt认证的方式来认证,框架就可以完成验签与claims的解析。

[ApiController] [Route("[controller]")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public class ProductController : ControllerBase { private static readonly Product[] Products = [ new Product { Id = 1, Name = "机械键盘", Price = 599.00m } ]; [HttpGet("{id:int}", Name = "GetProductById")] public ActionResult GetById(int id) { var product = Products.FirstOrDefault(p => p.Id == id); if (product is null) { return NotFound(); } return product; } }

那么集成进来之后,当有接口请求时,就会按照如下流程来执行:

因为定义了未授权事件,一旦触发就会收到友好的提示: