如何控制ASP.NET Core Cookie认证序列化以减小Cookie体积?
针对你遇到的ASP.NET Core 2.2 MVC OIDC Cookie体积超标、被拆分后Chrome偶尔拒绝的问题,结合你分析出的几个核心原因,我给你整理了针对性的解决方案:
1. 重新解析id_token生成精简Claim,避免冗余序列化
默认情况下,ASP.NET Core OIDC中间件会给每个Claim带上完整的颁发者URI和XML数据类型URI(比如http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name),这些冗余信息会大幅增加Cookie体积。我们可以在OnTokenValidated事件里手动解析id_token,生成只包含必要信息的精简Claim:
.AddOpenIdConnect("Oidc", o => { // 你的其他OIDC配置... o.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { var jwtHandler = new JwtSecurityTokenHandler(); if (jwtHandler.ReadJwtToken(context.SecurityToken.RawData) is JwtSecurityToken jwtToken) { // 只保留你需要的声明,自定义ValueType和Issuer(用短标识替代长URI) var trimmedClaims = jwtToken.Claims .Where(c => new[] { "sub", "name", "email", "role" }.Contains(c.Type)) // 按需过滤 .Select(c => new Claim(c.Type, c.Value, ClaimValueTypes.String, "MyAppIssuer")); // 替换原Identity,只保留精简后的Claim var newIdentity = new ClaimsIdentity(trimmedClaims, context.Principal.Identity.AuthenticationType); context.Principal = new ClaimsPrincipal(newIdentity); } await Task.CompletedTask; } }; });
这样每个Claim的序列化体积会从原来的~1900字节大幅压缩,直接解决冗余URI的问题。
2. 配置User Info Endpoint获取精准身份资源
ASP.NET Core OIDC中间件默认会调用User Info Endpoint,但你可以通过配置Scope和事件处理来获取并只保留你需要的声明:
.AddOpenIdConnect("Oidc", o => { // 清空默认Scope,只添加你需要的资源 o.Scope.Clear(); o.Scope.Add("openid"); // 必须的基础Scope o.Scope.Add("profile"); // 按需添加 o.Scope.Add("email"); // 启用User Info Endpoint调用 o.GetClaimsFromUserInfoEndpoint = true; o.Events.OnUserInformationReceived = context => { // 过滤冗余声明,避免重复添加 var existingClaimTypes = context.Principal.Claims.Select(c => c.Type).ToList(); foreach (var claim in context.User.RootElement.EnumerateObject()) { var claimType = claim.Name; if (!existingClaimTypes.Contains(claimType)) { context.Principal.Identities.First().AddClaim( new Claim(claimType, claim.Value.ToString(), ClaimValueTypes.String)); } } // 移除不需要的默认声明(比如一些OIDC自带的冗余字段) var redundantClaims = context.Principal.Claims.Where(c => c.Type.StartsWith("nonce") || c.Type == "aud").ToList(); foreach (var c in redundantClaims) { context.Principal.Identities.First().RemoveClaim(c); } return Task.CompletedTask; }; });
通过这种方式,你可以精准控制从User Info Endpoint获取的内容,避免不必要的声明占用Cookie空间。
3. 自定义高效的Claim序列化方式
ASP.NET Core默认的Ticket序列化方式比较冗余,你可以替换成更高效的JSON序列化(比如用Newtonsoft.Json):
首先实现自定义的ITicketSerializer:
public class JsonTicketSerializer : ITicketSerializer { private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.None, ContractResolver = new CamelCasePropertyNamesContractResolver() }; public byte[] Serialize(AuthenticationTicket ticket) { var json = JsonConvert.SerializeObject(ticket, _settings); return Encoding.UTF8.GetBytes(json); } public AuthenticationTicket Deserialize(byte[] data) { var json = Encoding.UTF8.GetString(data); return JsonConvert.DeserializeObject<AuthenticationTicket>(json, _settings); } }
然后在Cookie配置中替换默认的序列化器:
services.AddAuthentication() .AddCookie("Cookies", o => { // 你的其他Cookie配置... var dataProtector = services.BuildServiceProvider().GetDataProtector( "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "Cookies", "v2"); o.TicketDataFormat = new TicketDataFormat(dataProtector, new JsonTicketSerializer()); }) .AddOpenIdConnect("Oidc", o => { /* 你的OIDC配置 */ });
JSON序列化比默认的二进制序列化更紧凑,能有效减少Cookie体积。
4. 避免access_token等的双重Base64编码
默认情况下,OIDC中间件会把已Base64编码的token(比如JWT)直接存在AuthenticationProperties中,序列化时会再次进行Base64编码,导致体积翻倍。解决方法有两种:
方案一:将token存储到服务器端(推荐)
把access_token、id_token存到Redis等缓存中,Cookie里只存缓存的key:
o.Events.OnTokenValidated = async context => { // 用Redis存储access_token var redisKey = $"token:{Guid.NewGuid()}"; await _redisCache.SetStringAsync(redisKey, context.TokenResponse.AccessToken, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); // 把key存入Cookie,移除原始token条目 context.Properties.SetString("access_token_key", redisKey); context.Properties.Items.Remove(".Token.access_token"); context.Properties.Items.Remove(".Token.id_token"); await Task.CompletedTask; };
登出时通过key从Redis取出token即可完成OIDC登出流程。
方案二:解码后存储原始内容
如果必须存在Cookie里,先解码token的payload再存储,避免双重编码:
o.Events.OnTokenValidated = async context => { var jwtHandler = new JwtSecurityTokenHandler(); var jwtToken = jwtHandler.ReadJwtToken(context.SecurityToken.RawData); // 存储payload的JSON字符串,而非Base64的原始token context.Properties.SetString("id_token_payload", JsonConvert.SerializeObject(jwtToken.Payload)); context.Properties.Items.Remove(".Token.id_token"); await Task.CompletedTask; };
额外优化:限制单个Cookie大小
在Cookie配置中设置单个Cookie的最大大小,避免分片:
.AddCookie("Cookies", o => { o.Cookie.SizeLimit = 4096; // Chrome的单个Cookie大小限制为4096字节 // 其他配置... })
内容的提问来源于stack exchange,提问作者Dai

