找回密码
 立即注册
首页 业界区 业界 ClaimsPrincipal序列化为Json的正确姿势

ClaimsPrincipal序列化为Json的正确姿势

纪音悦 2 小时前
在现代 .NET 应用(尤其是 ASP.NET Core)中,ClaimsPrincipal 是身份认证和授权的核心对象。它封装了当前用户的身份信息、角色声明(claims)以及其他安全上下文数据。然而,当你需要将用户身份信息跨服务传递、记录日志、缓存或用于调试时,往往会遇到一个看似简单却颇具挑战的问题:ClaimsPrincipal 无法直接被标准的 JSON 序列化器(如 System.Text.Json 或 Newtonsoft.Json)序列化。
为什么?因为 ClaimsPrincipal 并非设计为可序列化的 DTO(Data Transfer Object,数据传输对象),其内部结构包含接口、只读集合和运行时类型,缺乏公开的构造函数和属性 setter。那么,我们该如何优雅地将其转换为 JSON 字符串,并在需要时还原回来?
本文将带你一步步探索可行的解决方案——从手动提取关键声明,到自定义转换器,再到完整可逆的序列化/反序列化策略,助你在分布式系统、微服务通信或审计日志等场景中灵活处理用户身份信息。
第一步,看源码,分析类的结构

不用去反编译,Github上有现成的:https://github.com/dotnet/runtime/tree/main/src/libraries/System.Security.Claims/src/System/Security/Claims
就不挨个贴代码了,我直接整理成类图。
---title: ClaimsPrincipal---classDiagram    class Claim {        +String Type        +String Value        +String ValueType        +String Issuer        +String OriginalIssuer        +Properties: IDictionary~string, string~        +Claim(string type, string value)        +Claim(string type, string value, string valueType)        +ToString() string    }    class ClaimsIdentity {        +String AuthenticationType        +bool IsAuthenticated        +String Name        +String NameClaimType        +String RoleClaimType        +IEnumerable~Claim~ Claims        +ClaimsIdentity()        +ClaimsIdentity(string authenticationType)        +ClaimsIdentity(IEnumerable~Claim~ claims)        +AddClaim(Claim claim) void        +FindFirst(string type) Claim        +HasClaim(string type, string value) bool        +HasClaim(Predicate~Claim~ match) bool    }    class ClaimsPrincipal {        +IEnumerable~ClaimsIdentity~ Identities        +ClaimsIdentity Identity        +IEnumerable~Claim~ Claims        +string Identity.AuthenticationType        +bool Identity.IsAuthenticated        +string Identity.Name        +ClaimsPrincipal()        +ClaimsPrincipal(ClaimsIdentity identity)        +ClaimsPrincipal(IEnumerable~ClaimsIdentity~ identities)        +FindFirst(string claimType) Claim        +HasClaim(string type, string value) bool        +IsInRole(string role) bool    }    ClaimsPrincipal "1" *-- "0..*" ClaimsIdentity : contains    ClaimsIdentity "1" *-- "0..*" Claim : contains第二步,理解三者的关系

1. Claim:声明的基本单元

职责


  • 表示一个键值对形式的声明(如 "name" = "Alice"、"role" = "Admin")。
  • 不仅包含类型(Type)和值(Value),还携带元数据,如:

    • ValueType:值的数据类型(如字符串、整数等,默认为 string)。
    • Issuer:声明的颁发者(如 "https://login.microsoft.com")。
    • OriginalIssuer:原始颁发者(用于联合身份场景)。
    • Properties:可扩展的键值对字典,用于存储额外信息。

特点


  • 不可变性:一旦创建,其核心属性(Type/Value)通常不可更改。
  • 轻量级:设计为数据载体,无业务逻辑。
  • 语义丰富:通过标准声明类型(如 ClaimTypes.Name、ClaimTypes.Role)实现互操作性。
2. ClaimsIdentity:代表一个身份(Identity)

职责


  • 封装一组相关的 Claim,构成一个完整的身份视图
  • 提供身份的上下文信息:

    • AuthenticationType:认证方式(如 "Cookies"、"JwtBearer")。
    • IsAuthenticated:是否已通过认证(依赖 AuthenticationType 非空)。
    • Name:通常映射自 NameClaimType 类型的声明(默认为 ClaimTypes.Name)。
    • RoleClaimType:用于角色判断的声明类型(默认为 ClaimTypes.Role)。

关键行为


  • AddClaim(Claim):动态添加声明(常用于中间件或策略授权)。
  • FindFirst(string type):查找首个匹配类型的声明。
  • HasClaim(...):检查是否存在特定声明。
设计思想


  • 一个主体(如用户)可以有多个身份(例如:本地登录身份 + 社交账号身份)。
  • 支持多身份源(如 Windows 身份、JWT 身份、自定义身份)。
3. ClaimsPrincipal:代表一个安全主体(Principal)

职责


  • 代表当前用户或系统实体,是安全上下文的顶层容器。
  • 包含一个或多个 ClaimsIdentity 对象(通过 Identities 集合)。
  • 提供便捷属性访问主身份(Identity 属性返回第一个 ClaimsIdentity)。
  • 聚合所有身份的声明(Claims 属性返回所有 ClaimsIdentity 中声明的并集)。
关键行为


  • IsInRole(string role):检查是否属于某角色(依据 RoleClaimType 声明)。
  • FindFirst(string claimType):跨所有身份查找首个匹配声明。
  • HasClaim(...):检查是否存在指定声明。
与线程上下文集成


  • 在 ASP.NET Core 中,HttpContext.User 的类型就是 ClaimsPrincipal。
  • 可通过 Thread.CurrentPrincipal(.NET Framework)或 IHttpContextAccessor(.NET Core)访问。
4. 三者协作关系
  1. ClaimsPrincipal
  2. ├── ClaimsIdentity #1 (e.g., from JWT)
  3. │   ├── Claim: { Type="name", Value="Alice" }
  4. │   ├── Claim: { Type="role", Value="Admin" }
  5. │   └── Claim: { Type="email", Value="alice@example.com" }
  6. └── ClaimsIdentity #2 (e.g., from external provider)
  7.     ├── Claim: { Type="sub", Value="12345" }
  8.     └── Claim: { Type="idp", Value="Google" }
复制代码
第三步,设计序列化模型(DTO)

理解了 ClaimsPrincipal 的内部结构后,我们需要设计可序列化的 DTO 来承载这些数据。关键是要保留足够的信息,以便在反序列化时能够完整还原原始对象。
1. 设计原则

在设计序列化模型时,我们需要遵循以下原则:
1.1 完整性(Completeness)

保留所有关键信息,确保反序列化后的对象与原始对象等价:

  • Claim 的所有核心属性(Type、Value、ValueType、Issuer、OriginalIssuer、Properties)
  • ClaimsIdentity 的认证上下文(AuthenticationType、NameClaimType、RoleClaimType)
  • ClaimsPrincipal 的多身份支持(Identities 集合)
1.2 可序列化性(Serializability)


  • 使用简单类型(string、int、bool 等)和标准集合(List、Dictionary)
  • 避免接口类型、只读属性、私有构造函数等不友好的序列化特性
  • 兼容主流序列化器(System.Text.Json、Newtonsoft.Json)
1.3 可读性(Readability)

生成的 JSON 应当清晰易读,方便调试和日志记录:
  1. {
  2.   "identities": [
  3.     {
  4.       "authenticationType": "Bearer",
  5.       "nameClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
  6.       "roleClaimType": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
  7.       "claims": [
  8.         {
  9.           "type": "name",
  10.           "value": "Alice",
  11.           "valueType": "http://www.w3.org/2001/XMLSchema#string",
  12.           "issuer": "LOCAL AUTHORITY"
  13.         }
  14.       ]
  15.     }
  16.   ]
  17. }
复制代码
2. DTO 类设计

2.1 ClaimDto
  1. public class ClaimDto
  2. {
  3.     /// <summary>
  4.     /// 声明类型(如 "name"、"role"、"email")
  5.     /// </summary>
  6.     public string Type { get; set; } = string.Empty;
  7.     /// <summary>
  8.     /// 声明值
  9.     /// </summary>
  10.     public string Value { get; set; } = string.Empty;
  11.     /// <summary>
  12.     /// 值的数据类型(默认为 XMLSchema#string)
  13.     /// </summary>
  14.     public string ValueType { get; set; } = ClaimValueTypes.String;
  15.     /// <summary>
  16.     /// 声明颁发者
  17.     /// </summary>
  18.     public string Issuer { get; set; } = ClaimsIdentity.DefaultIssuer;
  19.     /// <summary>
  20.     /// 原始颁发者(联合身份场景)
  21.     /// </summary>
  22.     public string OriginalIssuer { get; set; } = ClaimsIdentity.DefaultIssuer;
  23.     /// <summary>
  24.     /// 自定义属性集合
  25.     /// </summary>
  26.     public Dictionary<string, string>? Properties { get; set; }
  27.     /// <summary>
  28.     /// 从 Claim 创建 DTO
  29.     /// </summary>
  30.     public static ClaimDto FromClaim(Claim claim)
  31.     {
  32.         var dto = new ClaimDto
  33.         {
  34.             Type = claim.Type,
  35.             Value = claim.Value,
  36.             ValueType = claim.ValueType,
  37.             Issuer = claim.Issuer,
  38.             OriginalIssuer = claim.OriginalIssuer
  39.         };
  40.         // 仅在有自定义属性时才序列化
  41.         if (claim.Properties.Count > 0)
  42.         {
  43.             dto.Properties = new Dictionary<string, string>(claim.Properties);
  44.         }
  45.         return dto;
  46.     }
  47.     /// <summary>
  48.     /// 转换为 Claim 对象
  49.     /// </summary>
  50.     public Claim ToClaim()
  51.     {
  52.         var claim = new Claim(Type, Value, ValueType, Issuer, OriginalIssuer);
  53.         // 还原自定义属性
  54.         if (Properties != null)
  55.         {
  56.             foreach (var prop in Properties)
  57.             {
  58.                 claim.Properties[prop.Key] = prop.Value;
  59.             }
  60.         }
  61.         return claim;
  62.     }
  63. }
复制代码
2.2 ClaimsIdentityDto
  1. public class ClaimsIdentityDto
  2. {
  3.     /// <summary>
  4.     /// 认证类型(如 "Bearer"、"Cookies")
  5.     /// </summary>
  6.     public string? AuthenticationType { get; set; }
  7.     /// <summary>
  8.     /// 用于标识用户名的声明类型
  9.     /// </summary>
  10.     public string NameClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;
  11.     /// <summary>
  12.     /// 用于标识角色的声明类型
  13.     /// </summary>
  14.     public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType;
  15.     /// <summary>
  16.     /// 声明集合
  17.     /// </summary>
  18.     public List<ClaimDto> Claims { get; set; } = new();
  19.     /// <summary>
  20.     /// 从 ClaimsIdentity 创建 DTO
  21.     /// </summary>
  22.     public static ClaimsIdentityDto FromClaimsIdentity(ClaimsIdentity identity)
  23.     {
  24.         return new ClaimsIdentityDto
  25.         {
  26.             AuthenticationType = identity.AuthenticationType,
  27.             NameClaimType = identity.NameClaimType,
  28.             RoleClaimType = identity.RoleClaimType,
  29.             Claims = identity.Claims.Select(ClaimDto.FromClaim).ToList()
  30.         };
  31.     }
  32.     /// <summary>
  33.     /// 转换为 ClaimsIdentity 对象
  34.     /// </summary>
  35.     public ClaimsIdentity ToClaimsIdentity()
  36.     {
  37.         var claims = Claims.Select(c => c.ToClaim()).ToList();
  38.         return new ClaimsIdentity(
  39.             claims,
  40.             AuthenticationType,
  41.             NameClaimType,
  42.             RoleClaimType
  43.         );
  44.     }
  45. }
复制代码
2.3 ClaimsPrincipalDto
  1. public class ClaimsPrincipalDto
  2. {
  3.     /// <summary>
  4.     /// 身份集合
  5.     /// </summary>
  6.     public List<ClaimsIdentityDto> Identities { get; set; } = new();
  7.     /// <summary>
  8.     /// 从 ClaimsPrincipal 创建 DTO
  9.     /// </summary>
  10.     public static ClaimsPrincipalDto FromClaimsPrincipal(ClaimsPrincipal principal)
  11.     {
  12.         return new ClaimsPrincipalDto
  13.         {
  14.             Identities = principal.Identities
  15.                 .Select(ClaimsIdentityDto.FromClaimsIdentity)
  16.                 .ToList()
  17.         };
  18.     }
  19.     /// <summary>
  20.     /// 转换为 ClaimsPrincipal 对象
  21.     /// </summary>
  22.     public ClaimsPrincipal ToClaimsPrincipal()
  23.     {
  24.         var identities = Identities.Select(i => i.ToClaimsIdentity()).ToList();
  25.         return new ClaimsPrincipal(identities);
  26.     }
  27. }
复制代码
3. 使用示例

3.1 序列化
  1. using System.Text.Json;
  2. // 获取当前用户的 ClaimsPrincipal
  3. var principal = httpContext.User;
  4. // 转换为 DTO
  5. var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
  6. // 序列化为 JSON
  7. var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions
  8. {
  9.     WriteIndented = true,  // 格式化输出
  10.     DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull  // 忽略 null 值
  11. });
  12. // 记录到日志或存储
  13. logger.LogInformation("User principal: {Principal}", json);
复制代码
3.2 反序列化
  1. // 从 JSON 还原
  2. var dto = JsonSerializer.Deserialize<ClaimsPrincipalDto>(json);
  3. // 转换为 ClaimsPrincipal
  4. var principal = dto?.ToClaimsPrincipal();
  5. // 使用还原的对象
  6. if (principal != null)
  7. {
  8.     httpContext.User = principal;
  9.     // 或者传递给其他服务
  10. }
复制代码
3.3 实际应用场景

场景 1:分布式会话存储
  1. // 将用户身份序列化后存储到 Redis
  2. public async Task SaveUserSession(string sessionId, ClaimsPrincipal principal)
  3. {
  4.     var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
  5.     var json = JsonSerializer.Serialize(dto);
  6.     await redis.StringSetAsync($"session:{sessionId}", json, TimeSpan.FromHours(1));
  7. }
  8. // 从 Redis 还原用户身份
  9. public async Task<ClaimsPrincipal?> LoadUserSession(string sessionId)
  10. {
  11.     var json = await redis.StringGetAsync($"session:{sessionId}");
  12.     if (json.IsNullOrEmpty) return null;
  13.    
  14.     var dto = JsonSerializer.Deserialize<ClaimsPrincipalDto>(json);
  15.     return dto?.ToClaimsPrincipal();
  16. }
复制代码
场景 2:微服务间传递身份
  1. // 在 HTTP 请求头中传递用户身份
  2. public async Task<HttpResponseMessage> CallDownstreamService(ClaimsPrincipal principal)
  3. {
  4.     var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
  5.     var json = JsonSerializer.Serialize(dto);
  6.    
  7.     var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
  8.     request.Headers.Add("X-User-Principal", Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
  9.    
  10.     return await httpClient.SendAsync(request);
  11. }
复制代码
场景 3:审计日志
  1. // 记录用户操作时保存完整的身份信息
  2. public void LogUserAction(ClaimsPrincipal principal, string action, object data)
  3. {
  4.     var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
  5.     var auditLog = new
  6.     {
  7.         Timestamp = DateTime.UtcNow,
  8.         Principal = dto,
  9.         Action = action,
  10.         Data = data
  11.     };
  12.    
  13.     var json = JsonSerializer.Serialize(auditLog, new JsonSerializerOptions { WriteIndented = true });
  14.     File.AppendAllText("audit.log", json + Environment.NewLine);
  15. }
复制代码
4. 性能优化建议

4.1 按需序列化

如果只需要部分信息(如用户名和角色),可以创建简化版的 DTO:
  1. public class SimplePrincipalDto
  2. {
  3.     public string? Name { get; set; }
  4.     public List<string> Roles { get; set; } = new();
  5.    
  6.     public static SimplePrincipalDto FromClaimsPrincipal(ClaimsPrincipal principal)
  7.     {
  8.         return new SimplePrincipalDto
  9.         {
  10.             Name = principal.Identity?.Name,
  11.             Roles = principal.Claims
  12.                 .Where(c => c.Type == ClaimTypes.Role)
  13.                 .Select(c => c.Value)
  14.                 .ToList()
  15.         };
  16.     }
  17. }
复制代码
4.2 缓存序列化结果

对于频繁访问的用户身份信息,可以缓存序列化结果:
  1. private readonly ConcurrentDictionary<string, string> _cache = new();
  2. public string GetCachedJson(ClaimsPrincipal principal)
  3. {
  4.     var key = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
  5.     return _cache.GetOrAdd(key, _ =>
  6.     {
  7.         var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
  8.         return JsonSerializer.Serialize(dto);
  9.     });
  10. }
复制代码
4.3 避免序列化敏感信息

在序列化前过滤敏感声明:
  1. public static ClaimsPrincipalDto FromClaimsPrincipalSafe(ClaimsPrincipal principal)
  2. {
  3.     // 定义敏感声明类型
  4.     var sensitiveTypes = new HashSet<string>
  5.     {
  6.         "password",
  7.         "secret",
  8.         "token"
  9.     };
  10.    
  11.     var dto = new ClaimsPrincipalDto
  12.     {
  13.         Identities = principal.Identities.Select(identity => new ClaimsIdentityDto
  14.         {
  15.             AuthenticationType = identity.AuthenticationType,
  16.             NameClaimType = identity.NameClaimType,
  17.             RoleClaimType = identity.RoleClaimType,
  18.             Claims = identity.Claims
  19.                 .Where(c => !sensitiveTypes.Contains(c.Type.ToLowerInvariant()))
  20.                 .Select(ClaimDto.FromClaim)
  21.                 .ToList()
  22.         }).ToList()
  23.     };
  24.    
  25.     return dto;
  26. }
复制代码
第四步,通过自定义 Converter 进行序列化和反序列化

上一步我们使用 DTO 模式实现了序列化,需要手动转换对象。如果你希望更透明地序列化 ClaimsPrincipal,可以使用 System.Text.Json 的自定义转换器(JsonConverter)。这样可以直接对原始对象进行序列化,无需中间的 DTO 层。
1. 自定义 Converter 的优势

1.1 透明性

直接序列化原始类型,无需手动转换:
  1. // DTO 方式(需要显式转换)
  2. var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
  3. var json = JsonSerializer.Serialize(dto);
  4. // Converter 方式(直接序列化)
  5. var json = JsonSerializer.Serialize(principal, options);
复制代码
1.2 集中化

序列化逻辑集中在 Converter 中,使用时只需配置一次:
  1. var options = new JsonSerializerOptions();
  2. options.Converters.Add(new ClaimsPrincipalConverter());
  3. // 全局使用,无需每次转换
复制代码
1.3 类型安全

反序列化直接返回目标类型,无需额外转换:
  1. var principal = JsonSerializer.Deserialize<ClaimsPrincipal>(json, options);
  2. // 直接得到 ClaimsPrincipal,不是 DTO
复制代码
2. 实现自定义 Converter

2.1 ClaimConverter
  1. using System.Security.Claims;
  2. using System.Text.Json;
  3. using System.Text.Json.Serialization;
  4. public class ClaimConverter : JsonConverter<Claim>
  5. {
  6.     public override Claim? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  7.     {
  8.         if (reader.TokenType != JsonTokenType.StartObject)
  9.         {
  10.             throw new JsonException("Expected StartObject token");
  11.         }
  12.         string? type = null;
  13.         string? value = null;
  14.         string? valueType = ClaimValueTypes.String;
  15.         string? issuer = ClaimsIdentity.DefaultIssuer;
  16.         string? originalIssuer = ClaimsIdentity.DefaultIssuer;
  17.         Dictionary<string, string>? properties = null;
  18.         while (reader.Read())
  19.         {
  20.             if (reader.TokenType == JsonTokenType.EndObject)
  21.             {
  22.                 break;
  23.             }
  24.             if (reader.TokenType != JsonTokenType.PropertyName)
  25.             {
  26.                 throw new JsonException("Expected PropertyName token");
  27.             }
  28.             string propertyName = reader.GetString()!;
  29.             reader.Read();
  30.             switch (propertyName.ToLowerInvariant())
  31.             {
  32.                 case "type":
  33.                     type = reader.GetString();
  34.                     break;
  35.                 case "value":
  36.                     value = reader.GetString();
  37.                     break;
  38.                 case "valuetype":
  39.                     valueType = reader.GetString() ?? ClaimValueTypes.String;
  40.                     break;
  41.                 case "issuer":
  42.                     issuer = reader.GetString() ?? ClaimsIdentity.DefaultIssuer;
  43.                     break;
  44.                 case "originalissuer":
  45.                     originalIssuer = reader.GetString() ?? ClaimsIdentity.DefaultIssuer;
  46.                     break;
  47.                 case "properties":
  48.                     properties = JsonSerializer.Deserialize<Dictionary<string, string>>(ref reader, options);
  49.                     break;
  50.                 default:
  51.                     reader.Skip();
  52.                     break;
  53.             }
  54.         }
  55.         if (string.IsNullOrEmpty(type) || value == null)
  56.         {
  57.             throw new JsonException("Claim must have Type and Value");
  58.         }
  59.         var claim = new Claim(type, value, valueType, issuer, originalIssuer);
  60.         if (properties != null)
  61.         {
  62.             foreach (var prop in properties)
  63.             {
  64.                 claim.Properties[prop.Key] = prop.Value;
  65.             }
  66.         }
  67.         return claim;
  68.     }
  69.     public override void Write(Utf8JsonWriter writer, Claim value, JsonSerializerOptions options)
  70.     {
  71.         writer.WriteStartObject();
  72.         
  73.         writer.WriteString("type", value.Type);
  74.         writer.WriteString("value", value.Value);
  75.         writer.WriteString("valueType", value.ValueType);
  76.         writer.WriteString("issuer", value.Issuer);
  77.         writer.WriteString("originalIssuer", value.OriginalIssuer);
  78.         if (value.Properties.Count > 0)
  79.         {
  80.             writer.WritePropertyName("properties");
  81.             JsonSerializer.Serialize(writer, value.Properties, options);
  82.         }
  83.         writer.WriteEndObject();
  84.     }
  85. }
复制代码
2.2 ClaimsIdentityConverter
  1. public class ClaimsIdentityConverter : JsonConverter<ClaimsIdentity>
  2. {
  3.     public override ClaimsIdentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  4.     {
  5.         if (reader.TokenType != JsonTokenType.StartObject)
  6.         {
  7.             throw new JsonException("Expected StartObject token");
  8.         }
  9.         string? authenticationType = null;
  10.         string nameClaimType = ClaimsIdentity.DefaultNameClaimType;
  11.         string roleClaimType = ClaimsIdentity.DefaultRoleClaimType;
  12.         List<Claim>? claims = null;
  13.         while (reader.Read())
  14.         {
  15.             if (reader.TokenType == JsonTokenType.EndObject)
  16.             {
  17.                 break;
  18.             }
  19.             if (reader.TokenType != JsonTokenType.PropertyName)
  20.             {
  21.                 throw new JsonException("Expected PropertyName token");
  22.             }
  23.             string propertyName = reader.GetString()!;
  24.             reader.Read();
  25.             switch (propertyName.ToLowerInvariant())
  26.             {
  27.                 case "authenticationtype":
  28.                     authenticationType = reader.GetString();
  29.                     break;
  30.                 case "nameclaimtype":
  31.                     nameClaimType = reader.GetString() ?? ClaimsIdentity.DefaultNameClaimType;
  32.                     break;
  33.                 case "roleclaimtype":
  34.                     roleClaimType = reader.GetString() ?? ClaimsIdentity.DefaultRoleClaimType;
  35.                     break;
  36.                 case "claims":
  37.                     claims = JsonSerializer.Deserialize<List<Claim>>(ref reader, options);
  38.                     break;
  39.                 default:
  40.                     reader.Skip();
  41.                     break;
  42.             }
  43.         }
  44.         return new ClaimsIdentity(
  45.             claims ?? new List<Claim>(),
  46.             authenticationType,
  47.             nameClaimType,
  48.             roleClaimType
  49.         );
  50.     }
  51.     public override void Write(Utf8JsonWriter writer, ClaimsIdentity value, JsonSerializerOptions options)
  52.     {
  53.         writer.WriteStartObject();
  54.         
  55.         writer.WriteString("authenticationType", value.AuthenticationType);
  56.         writer.WriteString("nameClaimType", value.NameClaimType);
  57.         writer.WriteString("roleClaimType", value.RoleClaimType);
  58.         
  59.         writer.WritePropertyName("claims");
  60.         JsonSerializer.Serialize(writer, value.Claims.ToList(), options);
  61.         writer.WriteEndObject();
  62.     }
  63. }
复制代码
2.3 ClaimsPrincipalConverter
  1. public class ClaimsPrincipalConverter : JsonConverter<ClaimsPrincipal>
  2. {
  3.     public override ClaimsPrincipal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  4.     {
  5.         if (reader.TokenType != JsonTokenType.StartObject)
  6.         {
  7.             throw new JsonException("Expected StartObject token");
  8.         }
  9.         List<ClaimsIdentity>? identities = null;
  10.         while (reader.Read())
  11.         {
  12.             if (reader.TokenType == JsonTokenType.EndObject)
  13.             {
  14.                 break;
  15.             }
  16.             if (reader.TokenType != JsonTokenType.PropertyName)
  17.             {
  18.                 throw new JsonException("Expected PropertyName token");
  19.             }
  20.             string propertyName = reader.GetString()!;
  21.             reader.Read();
  22.             if (propertyName.Equals("identities", StringComparison.OrdinalIgnoreCase))
  23.             {
  24.                 identities = JsonSerializer.Deserialize<List<ClaimsIdentity>>(ref reader, options);
  25.             }
  26.             else
  27.             {
  28.                 reader.Skip();
  29.             }
  30.         }
  31.         return new ClaimsPrincipal(identities ?? new List<ClaimsIdentity>());
  32.     }
  33.     public override void Write(Utf8JsonWriter writer, ClaimsPrincipal value, JsonSerializerOptions options)
  34.     {
  35.         writer.WriteStartObject();
  36.         
  37.         writer.WritePropertyName("identities");
  38.         JsonSerializer.Serialize(writer, value.Identities.ToList(), options);
  39.         writer.WriteEndObject();
  40.     }
  41. }
复制代码
3. 使用自定义 Converter

3.1 基本使用
  1. // 配置序列化选项
  2. var options = new JsonSerializerOptions
  3. {
  4.     WriteIndented = true,
  5.     DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
  6.     Converters =
  7.     {
  8.         new ClaimConverter(),
  9.         new ClaimsIdentityConverter(),
  10.         new ClaimsPrincipalConverter()
  11.     }
  12. };
  13. // 序列化
  14. var principal = httpContext.User;
  15. var json = JsonSerializer.Serialize(principal, options);
  16. // 反序列化
  17. var deserializedPrincipal = JsonSerializer.Deserialize<ClaimsPrincipal>(json, options);
复制代码
3.2 全局配置

在 ASP.NET Core 中,可以全局配置序列化选项:
  1. // Program.cs 或 Startup.cs
  2. builder.Services.Configure<JsonOptions>(options =>
  3. {
  4.     options.JsonSerializerOptions.Converters.Add(new ClaimConverter());
  5.     options.JsonSerializerOptions.Converters.Add(new ClaimsIdentityConverter());
  6.     options.JsonSerializerOptions.Converters.Add(new ClaimsPrincipalConverter());
  7. });
复制代码
3.3 创建扩展方法

为了更方便使用,可以创建扩展方法:
  1. public static class ClaimsPrincipalExtensions
  2. {
  3.     private static readonly JsonSerializerOptions DefaultOptions = new()
  4.     {
  5.         WriteIndented = true,
  6.         DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
  7.         Converters =
  8.         {
  9.             new ClaimConverter(),
  10.             new ClaimsIdentityConverter(),
  11.             new ClaimsPrincipalConverter()
  12.         }
  13.     };
  14.     /// <summary>
  15.     /// 将 ClaimsPrincipal 序列化为 JSON
  16.     /// </summary>
  17.     public static string ToJson(this ClaimsPrincipal principal, JsonSerializerOptions? options = null)
  18.     {
  19.         return JsonSerializer.Serialize(principal, options ?? DefaultOptions);
  20.     }
  21.     /// <summary>
  22.     /// 从 JSON 反序列化为 ClaimsPrincipal
  23.     /// </summary>
  24.     public static ClaimsPrincipal? FromJson(string json, JsonSerializerOptions? options = null)
  25.     {
  26.         return JsonSerializer.Deserialize<ClaimsPrincipal>(json, options ?? DefaultOptions);
  27.     }
  28.     /// <summary>
  29.     /// 将 ClaimsPrincipal 安全地序列化为 JSON(过滤敏感信息)
  30.     /// </summary>
  31.     public static string ToJsonSafe(this ClaimsPrincipal principal, HashSet<string>? sensitiveTypes = null)
  32.     {
  33.         sensitiveTypes ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase)
  34.         {
  35.             "password",
  36.             "secret",
  37.             "token",
  38.             "apikey"
  39.         };
  40.         // 创建过滤后的副本
  41.         var filteredIdentities = principal.Identities.Select(identity =>
  42.         {
  43.             var filteredClaims = identity.Claims
  44.                 .Where(c => !sensitiveTypes.Contains(c.Type))
  45.                 .ToList();
  46.             
  47.             return new ClaimsIdentity(
  48.                 filteredClaims,
  49.                 identity.AuthenticationType,
  50.                 identity.NameClaimType,
  51.                 identity.RoleClaimType
  52.             );
  53.         }).ToList();
  54.         var filteredPrincipal = new ClaimsPrincipal(filteredIdentities);
  55.         return filteredPrincipal.ToJson();
  56.     }
  57. }
复制代码
使用扩展方法:
  1. // 序列化
  2. var json = httpContext.User.ToJson();
  3. // 安全序列化(自动过滤敏感信息)
  4. var safeJson = httpContext.User.ToJsonSafe();
  5. // 反序列化
  6. var principal = ClaimsPrincipalExtensions.FromJson(json);
复制代码
4. 实际应用示例

4.1 中间件中传递用户身份
  1. public class UserPrincipalMiddleware
  2. {
  3.     private readonly RequestDelegate _next;
  4.     public UserPrincipalMiddleware(RequestDelegate next)
  5.     {
  6.         _next = next;
  7.     }
  8.     public async Task InvokeAsync(HttpContext context)
  9.     {
  10.         // 如果请求头包含序列化的用户身份,则还原
  11.         if (context.Request.Headers.TryGetValue("X-User-Principal", out var principalHeader))
  12.         {
  13.             try
  14.             {
  15.                 var json = Encoding.UTF8.GetString(Convert.FromBase64String(principalHeader!));
  16.                 var principal = ClaimsPrincipalExtensions.FromJson(json);
  17.                
  18.                 if (principal != null)
  19.                 {
  20.                     context.User = principal;
  21.                 }
  22.             }
  23.             catch (Exception ex)
  24.             {
  25.                 // 记录错误但不中断请求
  26.                 Console.WriteLine($"Failed to deserialize principal: {ex.Message}");
  27.             }
  28.         }
  29.         await _next(context);
  30.     }
  31. }
复制代码
4.2 分布式缓存
  1. public class UserSessionService
  2. {
  3.     private readonly IDistributedCache _cache;
  4.     public UserSessionService(IDistributedCache cache)
  5.     {
  6.         _cache = cache;
  7.     }
  8.     public async Task SaveSessionAsync(string sessionId, ClaimsPrincipal principal)
  9.     {
  10.         var json = principal.ToJson();
  11.         var bytes = Encoding.UTF8.GetBytes(json);
  12.         
  13.         await _cache.SetAsync(sessionId, bytes, new DistributedCacheEntryOptions
  14.         {
  15.             AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
  16.         });
  17.     }
  18.     public async Task<ClaimsPrincipal?> GetSessionAsync(string sessionId)
  19.     {
  20.         var bytes = await _cache.GetAsync(sessionId);
  21.         if (bytes == null) return null;
  22.         var json = Encoding.UTF8.GetString(bytes);
  23.         return ClaimsPrincipalExtensions.FromJson(json);
  24.     }
  25. }
复制代码
5. DTO vs Converter:如何选择?

5.1 使用 DTO 的场景

优点:

  • 显式转换,逻辑清晰
  • 可以灵活定制传输的数据结构
  • 更容易进行数据验证和转换
  • 不依赖特定的序列化框架
适用场景:

  • API 数据传输(需要版本控制和向后兼容)
  • 跨语言/跨平台通信
  • 需要数据脱敏或转换的场景
  • 长期存储的数据格式
示例:
  1. // 为前端提供简化的用户信息
  2. public class UserInfoDto
  3. {
  4.     public string UserId { get; set; }
  5.     public string Name { get; set; }
  6.     public List<string> Roles { get; set; }
  7.    
  8.     public static UserInfoDto FromPrincipal(ClaimsPrincipal principal)
  9.     {
  10.         return new UserInfoDto
  11.         {
  12.             UserId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "",
  13.             Name = principal.Identity?.Name ?? "",
  14.             Roles = principal.Claims
  15.                 .Where(c => c.Type == ClaimTypes.Role)
  16.                 .Select(c => c.Value)
  17.                 .ToList()
  18.         };
  19.     }
  20. }
复制代码
5.2 使用 Converter 的场景

优点:

  • 透明序列化,使用更简洁
  • 保持原始类型,无需转换
  • 全局配置一次,处处可用
  • 更好地支持嵌套对象序列化
适用场景:

  • 内部服务通信(同技术栈)
  • 临时缓存和会话存储
  • 调试和日志记录
  • 需要完整保留对象状态
示例:
  1. // 缓存完整的用户身份
  2. public async Task CacheUserPrincipal(string key, ClaimsPrincipal principal)
  3. {
  4.     // 直接序列化,无需转换
  5.     var json = principal.ToJson();
  6.     await _cache.SetStringAsync(key, json, TimeSpan.FromMinutes(30));
  7. }
复制代码
5.3 混合使用

在实际项目中,两种方式可以共存:
  1. public class UserService
  2. {
  3.     // 对外 API:使用 DTO
  4.     public UserInfoDto GetPublicUserInfo(ClaimsPrincipal principal)
  5.     {
  6.         return UserInfoDto.FromPrincipal(principal);
  7.     }
  8.     // 内部缓存:使用 Converter
  9.     public async Task CacheUserSession(string sessionId, ClaimsPrincipal principal)
  10.     {
  11.         var json = principal.ToJson();
  12.         await SaveToCache(sessionId, json);
  13.     }
  14.     // 审计日志:使用 Converter(安全模式)
  15.     public void LogUserAction(ClaimsPrincipal principal, string action)
  16.     {
  17.         var json = principal.ToJsonSafe();
  18.         _logger.LogInformation("Action: {Action}, User: {User}", action, json);
  19.     }
  20. }
复制代码
6. 性能对比
  1. using BenchmarkDotNet.Attributes;
  2. using BenchmarkDotNet.Running;
  3. [MemoryDiagnoser]
  4. public class SerializationBenchmark
  5. {
  6.     private ClaimsPrincipal _principal;
  7.     private JsonSerializerOptions _options;
  8.     [GlobalSetup]
  9.     public void Setup()
  10.     {
  11.         var claims = new[]
  12.         {
  13.             new Claim(ClaimTypes.Name, "Alice"),
  14.             new Claim(ClaimTypes.Email, "alice@example.com"),
  15.             new Claim(ClaimTypes.Role, "Admin"),
  16.             new Claim(ClaimTypes.Role, "User")
  17.         };
  18.         
  19.         var identity = new ClaimsIdentity(claims, "Bearer");
  20.         _principal = new ClaimsPrincipal(identity);
  21.         _options = new JsonSerializerOptions
  22.         {
  23.             Converters =
  24.             {
  25.                 new ClaimConverter(),
  26.                 new ClaimsIdentityConverter(),
  27.                 new ClaimsPrincipalConverter()
  28.             }
  29.         };
  30.     }
  31.     [Benchmark]
  32.     public string SerializeWithDto()
  33.     {
  34.         var dto = ClaimsPrincipalDto.FromClaimsPrincipal(_principal);
  35.         return JsonSerializer.Serialize(dto);
  36.     }
  37.     [Benchmark]
  38.     public string SerializeWithConverter()
  39.     {
  40.         return JsonSerializer.Serialize(_principal, _options);
  41.     }
  42.     [Benchmark]
  43.     public ClaimsPrincipal DeserializeWithDto()
  44.     {
  45.         var dto = ClaimsPrincipalDto.FromClaimsPrincipal(_principal);
  46.         var json = JsonSerializer.Serialize(dto);
  47.         var deserializedDto = JsonSerializer.Deserialize<ClaimsPrincipalDto>(json);
  48.         return deserializedDto!.ToClaimsPrincipal();
  49.     }
  50.     [Benchmark]
  51.     public ClaimsPrincipal DeserializeWithConverter()
  52.     {
  53.         var json = JsonSerializer.Serialize(_principal, _options);
  54.         return JsonSerializer.Deserialize<ClaimsPrincipal>(json, _options)!;
  55.     }
  56. }
  57. // 运行基准测试
  58. // BenchmarkRunner.Run<SerializationBenchmark>();
复制代码
预期结果:

  • Converter 方式通常略快(少一次对象转换)
  • 内存使用相近
  • 两者性能差异在大多数场景下可忽略
7. 注意事项

7.1 空值处理

确保 Converter 正确处理 null 值:
  1. public override void Write(Utf8JsonWriter writer, ClaimsPrincipal? value, JsonSerializerOptions options)
  2. {
  3.     if (value == null)
  4.     {
  5.         writer.WriteNullValue();
  6.         return;
  7.     }
  8.    
  9.     // ... 正常序列化逻辑
  10. }
复制代码
7.2 循环引用

虽然 ClaimsPrincipal 结构简单,不会出现循环引用,但在扩展时需注意:
  1. var options = new JsonSerializerOptions
  2. {
  3.     ReferenceHandler = ReferenceHandler.IgnoreCycles,  // 处理循环引用
  4.     Converters = { /* ... */ }
  5. };
复制代码
7.3 版本兼容性

序列化格式应考虑向后兼容:
  1. public override ClaimsIdentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  2. {
  3.     // 为新增字段提供默认值
  4.     string nameClaimType = ClaimsIdentity.DefaultNameClaimType;
  5.    
  6.     // 兼容旧版本 JSON(可能缺少某些字段)
  7.     // ...
  8. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册