ClaimsPrincipal序列化为Json的正确姿势

在现代 .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.NameClaimTypes.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. 三者协作关系

wiki 复制代码
ClaimsPrincipal
│
├── ClaimsIdentity #1 (e.g., from JWT)
│   ├── Claim: { Type="name", Value="Alice" }
│   ├── Claim: { Type="role", Value="Admin" }
│   └── Claim: { Type="email", Value="alice@example.com" }
│
└── ClaimsIdentity #2 (e.g., from external provider)
    ├── Claim: { Type="sub", Value="12345" }
    └── 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 应当清晰易读,方便调试和日志记录:

json 复制代码
{
  "identities": [
    {
      "authenticationType": "Bearer",
      "nameClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "roleClaimType": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
      "claims": [
        {
          "type": "name",
          "value": "Alice",
          "valueType": "http://www.w3.org/2001/XMLSchema#string",
          "issuer": "LOCAL AUTHORITY"
        }
      ]
    }
  ]
}

2. DTO 类设计

2.1 ClaimDto

csharp 复制代码
public class ClaimDto
{
    /// <summary>
    /// 声明类型(如 "name"、"role"、"email")
    /// </summary>
    public string Type { get; set; } = string.Empty;

    /// <summary>
    /// 声明值
    /// </summary>
    public string Value { get; set; } = string.Empty;

    /// <summary>
    /// 值的数据类型(默认为 XMLSchema#string)
    /// </summary>
    public string ValueType { get; set; } = ClaimValueTypes.String;

    /// <summary>
    /// 声明颁发者
    /// </summary>
    public string Issuer { get; set; } = ClaimsIdentity.DefaultIssuer;

    /// <summary>
    /// 原始颁发者(联合身份场景)
    /// </summary>
    public string OriginalIssuer { get; set; } = ClaimsIdentity.DefaultIssuer;

    /// <summary>
    /// 自定义属性集合
    /// </summary>
    public Dictionary<string, string>? Properties { get; set; }

    /// <summary>
    /// 从 Claim 创建 DTO
    /// </summary>
    public static ClaimDto FromClaim(Claim claim)
    {
        var dto = new ClaimDto
        {
            Type = claim.Type,
            Value = claim.Value,
            ValueType = claim.ValueType,
            Issuer = claim.Issuer,
            OriginalIssuer = claim.OriginalIssuer
        };

        // 仅在有自定义属性时才序列化
        if (claim.Properties.Count > 0)
        {
            dto.Properties = new Dictionary<string, string>(claim.Properties);
        }

        return dto;
    }

    /// <summary>
    /// 转换为 Claim 对象
    /// </summary>
    public Claim ToClaim()
    {
        var claim = new Claim(Type, Value, ValueType, Issuer, OriginalIssuer);

        // 还原自定义属性
        if (Properties != null)
        {
            foreach (var prop in Properties)
            {
                claim.Properties[prop.Key] = prop.Value;
            }
        }

        return claim;
    }
}

2.2 ClaimsIdentityDto

csharp 复制代码
public class ClaimsIdentityDto
{
    /// <summary>
    /// 认证类型(如 "Bearer"、"Cookies")
    /// </summary>
    public string? AuthenticationType { get; set; }

    /// <summary>
    /// 用于标识用户名的声明类型
    /// </summary>
    public string NameClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;

    /// <summary>
    /// 用于标识角色的声明类型
    /// </summary>
    public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType;

    /// <summary>
    /// 声明集合
    /// </summary>
    public List<ClaimDto> Claims { get; set; } = new();

    /// <summary>
    /// 从 ClaimsIdentity 创建 DTO
    /// </summary>
    public static ClaimsIdentityDto FromClaimsIdentity(ClaimsIdentity identity)
    {
        return new ClaimsIdentityDto
        {
            AuthenticationType = identity.AuthenticationType,
            NameClaimType = identity.NameClaimType,
            RoleClaimType = identity.RoleClaimType,
            Claims = identity.Claims.Select(ClaimDto.FromClaim).ToList()
        };
    }

    /// <summary>
    /// 转换为 ClaimsIdentity 对象
    /// </summary>
    public ClaimsIdentity ToClaimsIdentity()
    {
        var claims = Claims.Select(c => c.ToClaim()).ToList();
        return new ClaimsIdentity(
            claims,
            AuthenticationType,
            NameClaimType,
            RoleClaimType
        );
    }
}

2.3 ClaimsPrincipalDto

csharp 复制代码
public class ClaimsPrincipalDto
{
    /// <summary>
    /// 身份集合
    /// </summary>
    public List<ClaimsIdentityDto> Identities { get; set; } = new();

    /// <summary>
    /// 从 ClaimsPrincipal 创建 DTO
    /// </summary>
    public static ClaimsPrincipalDto FromClaimsPrincipal(ClaimsPrincipal principal)
    {
        return new ClaimsPrincipalDto
        {
            Identities = principal.Identities
                .Select(ClaimsIdentityDto.FromClaimsIdentity)
                .ToList()
        };
    }

    /// <summary>
    /// 转换为 ClaimsPrincipal 对象
    /// </summary>
    public ClaimsPrincipal ToClaimsPrincipal()
    {
        var identities = Identities.Select(i => i.ToClaimsIdentity()).ToList();
        return new ClaimsPrincipal(identities);
    }
}

3. 使用示例

3.1 序列化

csharp 复制代码
using System.Text.Json;

// 获取当前用户的 ClaimsPrincipal
var principal = httpContext.User;

// 转换为 DTO
var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);

// 序列化为 JSON
var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions
{
    WriteIndented = true,  // 格式化输出
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull  // 忽略 null 值
});

// 记录到日志或存储
logger.LogInformation("User principal: {Principal}", json);

3.2 反序列化

csharp 复制代码
// 从 JSON 还原
var dto = JsonSerializer.Deserialize<ClaimsPrincipalDto>(json);

// 转换为 ClaimsPrincipal
var principal = dto?.ToClaimsPrincipal();

// 使用还原的对象
if (principal != null)
{
    httpContext.User = principal;
    // 或者传递给其他服务
}

3.3 实际应用场景

场景 1:分布式会话存储

csharp 复制代码
// 将用户身份序列化后存储到 Redis
public async Task SaveUserSession(string sessionId, ClaimsPrincipal principal)
{
    var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
    var json = JsonSerializer.Serialize(dto);
    await redis.StringSetAsync($"session:{sessionId}", json, TimeSpan.FromHours(1));
}

// 从 Redis 还原用户身份
public async Task<ClaimsPrincipal?> LoadUserSession(string sessionId)
{
    var json = await redis.StringGetAsync($"session:{sessionId}");
    if (json.IsNullOrEmpty) return null;
    
    var dto = JsonSerializer.Deserialize<ClaimsPrincipalDto>(json);
    return dto?.ToClaimsPrincipal();
}

场景 2:微服务间传递身份

csharp 复制代码
// 在 HTTP 请求头中传递用户身份
public async Task<HttpResponseMessage> CallDownstreamService(ClaimsPrincipal principal)
{
    var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
    var json = JsonSerializer.Serialize(dto);
    
    var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
    request.Headers.Add("X-User-Principal", Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
    
    return await httpClient.SendAsync(request);
}

场景 3:审计日志

csharp 复制代码
// 记录用户操作时保存完整的身份信息
public void LogUserAction(ClaimsPrincipal principal, string action, object data)
{
    var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
    var auditLog = new
    {
        Timestamp = DateTime.UtcNow,
        Principal = dto,
        Action = action,
        Data = data
    };
    
    var json = JsonSerializer.Serialize(auditLog, new JsonSerializerOptions { WriteIndented = true });
    File.AppendAllText("audit.log", json + Environment.NewLine);
}

4. 性能优化建议

4.1 按需序列化

如果只需要部分信息(如用户名和角色),可以创建简化版的 DTO:

csharp 复制代码
public class SimplePrincipalDto
{
    public string? Name { get; set; }
    public List<string> Roles { get; set; } = new();
    
    public static SimplePrincipalDto FromClaimsPrincipal(ClaimsPrincipal principal)
    {
        return new SimplePrincipalDto
        {
            Name = principal.Identity?.Name,
            Roles = principal.Claims
                .Where(c => c.Type == ClaimTypes.Role)
                .Select(c => c.Value)
                .ToList()
        };
    }
}

4.2 缓存序列化结果

对于频繁访问的用户身份信息,可以缓存序列化结果:

csharp 复制代码
private readonly ConcurrentDictionary<string, string> _cache = new();

public string GetCachedJson(ClaimsPrincipal principal)
{
    var key = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
    return _cache.GetOrAdd(key, _ =>
    {
        var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
        return JsonSerializer.Serialize(dto);
    });
}

4.3 避免序列化敏感信息

在序列化前过滤敏感声明:

csharp 复制代码
public static ClaimsPrincipalDto FromClaimsPrincipalSafe(ClaimsPrincipal principal)
{
    // 定义敏感声明类型
    var sensitiveTypes = new HashSet<string>
    {
        "password",
        "secret",
        "token"
    };
    
    var dto = new ClaimsPrincipalDto
    {
        Identities = principal.Identities.Select(identity => new ClaimsIdentityDto
        {
            AuthenticationType = identity.AuthenticationType,
            NameClaimType = identity.NameClaimType,
            RoleClaimType = identity.RoleClaimType,
            Claims = identity.Claims
                .Where(c => !sensitiveTypes.Contains(c.Type.ToLowerInvariant()))
                .Select(ClaimDto.FromClaim)
                .ToList()
        }).ToList()
    };
    
    return dto;
}

第四步,通过自定义 Converter 进行序列化和反序列化

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

1. 自定义 Converter 的优势

1.1 透明性

直接序列化原始类型,无需手动转换:

csharp 复制代码
// DTO 方式(需要显式转换)
var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
var json = JsonSerializer.Serialize(dto);

// Converter 方式(直接序列化)
var json = JsonSerializer.Serialize(principal, options);

1.2 集中化

序列化逻辑集中在 Converter 中,使用时只需配置一次:

csharp 复制代码
var options = new JsonSerializerOptions();
options.Converters.Add(new ClaimsPrincipalConverter());
// 全局使用,无需每次转换

1.3 类型安全

反序列化直接返回目标类型,无需额外转换:

csharp 复制代码
var principal = JsonSerializer.Deserialize<ClaimsPrincipal>(json, options);
// 直接得到 ClaimsPrincipal,不是 DTO

2. 实现自定义 Converter

2.1 ClaimConverter

csharp 复制代码
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;

public class ClaimConverter : JsonConverter<Claim>
{
    public override Claim? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException("Expected StartObject token");
        }

        string? type = null;
        string? value = null;
        string? valueType = ClaimValueTypes.String;
        string? issuer = ClaimsIdentity.DefaultIssuer;
        string? originalIssuer = ClaimsIdentity.DefaultIssuer;
        Dictionary<string, string>? properties = null;

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                break;
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException("Expected PropertyName token");
            }

            string propertyName = reader.GetString()!;
            reader.Read();

            switch (propertyName.ToLowerInvariant())
            {
                case "type":
                    type = reader.GetString();
                    break;
                case "value":
                    value = reader.GetString();
                    break;
                case "valuetype":
                    valueType = reader.GetString() ?? ClaimValueTypes.String;
                    break;
                case "issuer":
                    issuer = reader.GetString() ?? ClaimsIdentity.DefaultIssuer;
                    break;
                case "originalissuer":
                    originalIssuer = reader.GetString() ?? ClaimsIdentity.DefaultIssuer;
                    break;
                case "properties":
                    properties = JsonSerializer.Deserialize<Dictionary<string, string>>(ref reader, options);
                    break;
                default:
                    reader.Skip();
                    break;
            }
        }

        if (string.IsNullOrEmpty(type) || value == null)
        {
            throw new JsonException("Claim must have Type and Value");
        }

        var claim = new Claim(type, value, valueType, issuer, originalIssuer);

        if (properties != null)
        {
            foreach (var prop in properties)
            {
                claim.Properties[prop.Key] = prop.Value;
            }
        }

        return claim;
    }

    public override void Write(Utf8JsonWriter writer, Claim value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        
        writer.WriteString("type", value.Type);
        writer.WriteString("value", value.Value);
        writer.WriteString("valueType", value.ValueType);
        writer.WriteString("issuer", value.Issuer);
        writer.WriteString("originalIssuer", value.OriginalIssuer);

        if (value.Properties.Count > 0)
        {
            writer.WritePropertyName("properties");
            JsonSerializer.Serialize(writer, value.Properties, options);
        }

        writer.WriteEndObject();
    }
}

2.2 ClaimsIdentityConverter

csharp 复制代码
public class ClaimsIdentityConverter : JsonConverter<ClaimsIdentity>
{
    public override ClaimsIdentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException("Expected StartObject token");
        }

        string? authenticationType = null;
        string nameClaimType = ClaimsIdentity.DefaultNameClaimType;
        string roleClaimType = ClaimsIdentity.DefaultRoleClaimType;
        List<Claim>? claims = null;

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                break;
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException("Expected PropertyName token");
            }

            string propertyName = reader.GetString()!;
            reader.Read();

            switch (propertyName.ToLowerInvariant())
            {
                case "authenticationtype":
                    authenticationType = reader.GetString();
                    break;
                case "nameclaimtype":
                    nameClaimType = reader.GetString() ?? ClaimsIdentity.DefaultNameClaimType;
                    break;
                case "roleclaimtype":
                    roleClaimType = reader.GetString() ?? ClaimsIdentity.DefaultRoleClaimType;
                    break;
                case "claims":
                    claims = JsonSerializer.Deserialize<List<Claim>>(ref reader, options);
                    break;
                default:
                    reader.Skip();
                    break;
            }
        }

        return new ClaimsIdentity(
            claims ?? new List<Claim>(),
            authenticationType,
            nameClaimType,
            roleClaimType
        );
    }

    public override void Write(Utf8JsonWriter writer, ClaimsIdentity value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        
        writer.WriteString("authenticationType", value.AuthenticationType);
        writer.WriteString("nameClaimType", value.NameClaimType);
        writer.WriteString("roleClaimType", value.RoleClaimType);
        
        writer.WritePropertyName("claims");
        JsonSerializer.Serialize(writer, value.Claims.ToList(), options);

        writer.WriteEndObject();
    }
}

2.3 ClaimsPrincipalConverter

csharp 复制代码
public class ClaimsPrincipalConverter : JsonConverter<ClaimsPrincipal>
{
    public override ClaimsPrincipal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException("Expected StartObject token");
        }

        List<ClaimsIdentity>? identities = null;

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                break;
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException("Expected PropertyName token");
            }

            string propertyName = reader.GetString()!;
            reader.Read();

            if (propertyName.Equals("identities", StringComparison.OrdinalIgnoreCase))
            {
                identities = JsonSerializer.Deserialize<List<ClaimsIdentity>>(ref reader, options);
            }
            else
            {
                reader.Skip();
            }
        }

        return new ClaimsPrincipal(identities ?? new List<ClaimsIdentity>());
    }

    public override void Write(Utf8JsonWriter writer, ClaimsPrincipal value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        
        writer.WritePropertyName("identities");
        JsonSerializer.Serialize(writer, value.Identities.ToList(), options);

        writer.WriteEndObject();
    }
}

3. 使用自定义 Converter

3.1 基本使用

csharp 复制代码
// 配置序列化选项
var options = new JsonSerializerOptions
{
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    Converters =
    {
        new ClaimConverter(),
        new ClaimsIdentityConverter(),
        new ClaimsPrincipalConverter()
    }
};

// 序列化
var principal = httpContext.User;
var json = JsonSerializer.Serialize(principal, options);

// 反序列化
var deserializedPrincipal = JsonSerializer.Deserialize<ClaimsPrincipal>(json, options);

3.2 全局配置

ASP.NET Core 中,可以全局配置序列化选项:

csharp 复制代码
// Program.cs 或 Startup.cs
builder.Services.Configure<JsonOptions>(options =>
{
    options.JsonSerializerOptions.Converters.Add(new ClaimConverter());
    options.JsonSerializerOptions.Converters.Add(new ClaimsIdentityConverter());
    options.JsonSerializerOptions.Converters.Add(new ClaimsPrincipalConverter());
});

3.3 创建扩展方法

为了更方便使用,可以创建扩展方法:

csharp 复制代码
public static class ClaimsPrincipalExtensions
{
    private static readonly JsonSerializerOptions DefaultOptions = new()
    {
        WriteIndented = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Converters =
        {
            new ClaimConverter(),
            new ClaimsIdentityConverter(),
            new ClaimsPrincipalConverter()
        }
    };

    /// <summary>
    /// 将 ClaimsPrincipal 序列化为 JSON
    /// </summary>
    public static string ToJson(this ClaimsPrincipal principal, JsonSerializerOptions? options = null)
    {
        return JsonSerializer.Serialize(principal, options ?? DefaultOptions);
    }

    /// <summary>
    /// 从 JSON 反序列化为 ClaimsPrincipal
    /// </summary>
    public static ClaimsPrincipal? FromJson(string json, JsonSerializerOptions? options = null)
    {
        return JsonSerializer.Deserialize<ClaimsPrincipal>(json, options ?? DefaultOptions);
    }

    /// <summary>
    /// 将 ClaimsPrincipal 安全地序列化为 JSON(过滤敏感信息)
    /// </summary>
    public static string ToJsonSafe(this ClaimsPrincipal principal, HashSet<string>? sensitiveTypes = null)
    {
        sensitiveTypes ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "password",
            "secret",
            "token",
            "apikey"
        };

        // 创建过滤后的副本
        var filteredIdentities = principal.Identities.Select(identity =>
        {
            var filteredClaims = identity.Claims
                .Where(c => !sensitiveTypes.Contains(c.Type))
                .ToList();
            
            return new ClaimsIdentity(
                filteredClaims,
                identity.AuthenticationType,
                identity.NameClaimType,
                identity.RoleClaimType
            );
        }).ToList();

        var filteredPrincipal = new ClaimsPrincipal(filteredIdentities);
        return filteredPrincipal.ToJson();
    }
}

使用扩展方法:

csharp 复制代码
// 序列化
var json = httpContext.User.ToJson();

// 安全序列化(自动过滤敏感信息)
var safeJson = httpContext.User.ToJsonSafe();

// 反序列化
var principal = ClaimsPrincipalExtensions.FromJson(json);

4. 实际应用示例

4.1 中间件中传递用户身份

csharp 复制代码
public class UserPrincipalMiddleware
{
    private readonly RequestDelegate _next;

    public UserPrincipalMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 如果请求头包含序列化的用户身份,则还原
        if (context.Request.Headers.TryGetValue("X-User-Principal", out var principalHeader))
        {
            try
            {
                var json = Encoding.UTF8.GetString(Convert.FromBase64String(principalHeader!));
                var principal = ClaimsPrincipalExtensions.FromJson(json);
                
                if (principal != null)
                {
                    context.User = principal;
                }
            }
            catch (Exception ex)
            {
                // 记录错误但不中断请求
                Console.WriteLine($"Failed to deserialize principal: {ex.Message}");
            }
        }

        await _next(context);
    }
}

4.2 分布式缓存

csharp 复制代码
public class UserSessionService
{
    private readonly IDistributedCache _cache;

    public UserSessionService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task SaveSessionAsync(string sessionId, ClaimsPrincipal principal)
    {
        var json = principal.ToJson();
        var bytes = Encoding.UTF8.GetBytes(json);
        
        await _cache.SetAsync(sessionId, bytes, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        });
    }

    public async Task<ClaimsPrincipal?> GetSessionAsync(string sessionId)
    {
        var bytes = await _cache.GetAsync(sessionId);
        if (bytes == null) return null;

        var json = Encoding.UTF8.GetString(bytes);
        return ClaimsPrincipalExtensions.FromJson(json);
    }
}

5. DTO vs Converter:如何选择?

5.1 使用 DTO 的场景

优点:

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

适用场景:

  • API 数据传输(需要版本控制和向后兼容)
  • 跨语言/跨平台通信
  • 需要数据脱敏或转换的场景
  • 长期存储的数据格式

示例:

csharp 复制代码
// 为前端提供简化的用户信息
public class UserInfoDto
{
    public string UserId { get; set; }
    public string Name { get; set; }
    public List<string> Roles { get; set; }
    
    public static UserInfoDto FromPrincipal(ClaimsPrincipal principal)
    {
        return new UserInfoDto
        {
            UserId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "",
            Name = principal.Identity?.Name ?? "",
            Roles = principal.Claims
                .Where(c => c.Type == ClaimTypes.Role)
                .Select(c => c.Value)
                .ToList()
        };
    }
}

5.2 使用 Converter 的场景

优点:

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

适用场景:

  • 内部服务通信(同技术栈)
  • 临时缓存和会话存储
  • 调试和日志记录
  • 需要完整保留对象状态

示例:

csharp 复制代码
// 缓存完整的用户身份
public async Task CacheUserPrincipal(string key, ClaimsPrincipal principal)
{
    // 直接序列化,无需转换
    var json = principal.ToJson();
    await _cache.SetStringAsync(key, json, TimeSpan.FromMinutes(30));
}

5.3 混合使用

在实际项目中,两种方式可以共存:

csharp 复制代码
public class UserService
{
    // 对外 API:使用 DTO
    public UserInfoDto GetPublicUserInfo(ClaimsPrincipal principal)
    {
        return UserInfoDto.FromPrincipal(principal);
    }

    // 内部缓存:使用 Converter
    public async Task CacheUserSession(string sessionId, ClaimsPrincipal principal)
    {
        var json = principal.ToJson();
        await SaveToCache(sessionId, json);
    }

    // 审计日志:使用 Converter(安全模式)
    public void LogUserAction(ClaimsPrincipal principal, string action)
    {
        var json = principal.ToJsonSafe();
        _logger.LogInformation("Action: {Action}, User: {User}", action, json);
    }
}

6. 性能对比

csharp 复制代码
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class SerializationBenchmark
{
    private ClaimsPrincipal _principal;
    private JsonSerializerOptions _options;

    [GlobalSetup]
    public void Setup()
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "Alice"),
            new Claim(ClaimTypes.Email, "alice@example.com"),
            new Claim(ClaimTypes.Role, "Admin"),
            new Claim(ClaimTypes.Role, "User")
        };
        
        var identity = new ClaimsIdentity(claims, "Bearer");
        _principal = new ClaimsPrincipal(identity);

        _options = new JsonSerializerOptions
        {
            Converters =
            {
                new ClaimConverter(),
                new ClaimsIdentityConverter(),
                new ClaimsPrincipalConverter()
            }
        };
    }

    [Benchmark]
    public string SerializeWithDto()
    {
        var dto = ClaimsPrincipalDto.FromClaimsPrincipal(_principal);
        return JsonSerializer.Serialize(dto);
    }

    [Benchmark]
    public string SerializeWithConverter()
    {
        return JsonSerializer.Serialize(_principal, _options);
    }

    [Benchmark]
    public ClaimsPrincipal DeserializeWithDto()
    {
        var dto = ClaimsPrincipalDto.FromClaimsPrincipal(_principal);
        var json = JsonSerializer.Serialize(dto);
        var deserializedDto = JsonSerializer.Deserialize<ClaimsPrincipalDto>(json);
        return deserializedDto!.ToClaimsPrincipal();
    }

    [Benchmark]
    public ClaimsPrincipal DeserializeWithConverter()
    {
        var json = JsonSerializer.Serialize(_principal, _options);
        return JsonSerializer.Deserialize<ClaimsPrincipal>(json, _options)!;
    }
}

// 运行基准测试
// BenchmarkRunner.Run<SerializationBenchmark>();

预期结果:

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

7. 注意事项

7.1 空值处理

确保 Converter 正确处理 null 值:

csharp 复制代码
public override void Write(Utf8JsonWriter writer, ClaimsPrincipal? value, JsonSerializerOptions options)
{
    if (value == null)
    {
        writer.WriteNullValue();
        return;
    }
    
    // ... 正常序列化逻辑
}

7.2 循环引用

虽然 ClaimsPrincipal 结构简单,不会出现循环引用,但在扩展时需注意:

csharp 复制代码
var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.IgnoreCycles,  // 处理循环引用
    Converters = { /* ... */ }
};

7.3 版本兼容性

序列化格式应考虑向后兼容:

csharp 复制代码
public override ClaimsIdentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    // 为新增字段提供默认值
    string nameClaimType = ClaimsIdentity.DefaultNameClaimType;
    
    // 兼容旧版本 JSON(可能缺少某些字段)
    // ...
}
相关推荐
追逐时光者12 小时前
一款使用 C# 编写专为 Windows 11 打造的文件资源管理器增强工具!
后端·.net
用户298698530143 天前
程序员效率工具:Spire.Doc如何助你一键搞定Word表格排版
后端·c#·.net
牧马人win3 天前
SmartDapper.Repository
.net
mudtools4 天前
搭建一套.net下能落地的飞书考勤系统
后端·c#·.net
玩泥巴的5 天前
搭建一套.net下能落地的飞书考勤系统
c#·.net·二次开发·飞书
快乐非自愿5 天前
C# 中的 Span 和内存:.NET 中的高性能内存处理
java·c#·.net