切链仆俏自定义 Converter 的优势
1.1 透明性
直接序列化原始类型,无需手动转换:
// DTO 方式(需要显式转换)
var dto = ClaimsPrincipalDto.FromClaimsPrincipal(principal);
var json = JsonSerializer.Serialize(dto);
// Converter 方式(直接序列化)
var json = JsonSerializer.Serialize(principal, options);
1.2 集中化
序列化逻辑集中在 Converter 中,使用时只需配置一次:
var options = new JsonSerializerOptions();
options.Converters.Add(new ClaimsPrincipalConverter());
// 全局使用,无需每次转换
1.3 类型安全
反序列化直接返回目标类型,无需额外转换:
var principal = JsonSerializer.Deserialize(json, options);
// 直接得到 ClaimsPrincipal,不是 DTO
- 实现自定义 Converter
2.1 ClaimConverter
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
public class ClaimConverter : JsonConverter
{
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? 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>(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
public class ClaimsIdentityConverter : JsonConverter
{
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? 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>(ref reader, options);
break;
default:
reader.Skip();
break;
}
}
return new ClaimsIdentity(
claims ?? new List(),
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
public class ClaimsPrincipalConverter : JsonConverter
{
public override ClaimsPrincipal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject token");
}
List? 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>(ref reader, options);
}
else
{
reader.Skip();
}
}
return new ClaimsPrincipal(identities ?? new List());
}
public override void Write(Utf8JsonWriter writer, ClaimsPrincipal value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName("identities");
JsonSerializer.Serialize(writer, value.Identities.ToList(), options);
writer.WriteEndObject();
}
}
- 使用自定义 Converter
3.1 基本使用
// 配置序列化选项
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(json, options);
3.2 全局配置
在 ASP.NET Core 中,可以全局配置序列化选项:
// Program.cs 或 Startup.cs
builder.Services.Configure(options =>
{
options.JsonSerializerOptions.Converters.Add(new ClaimConverter());
options.JsonSerializerOptions.Converters.Add(new ClaimsIdentityConverter());
options.JsonSerializerOptions.Converters.Add(new ClaimsPrincipalConverter());
});
3.3 创建扩展方法
为了更方便使用,可以创建扩展方法:
public static class ClaimsPrincipalExtensions
{
private static readonly JsonSerializerOptions DefaultOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters =
{
new ClaimConverter(),
new ClaimsIdentityConverter(),
new ClaimsPrincipalConverter()
}
};
///
/// 将 ClaimsPrincipal 序列化为 JSON
///
public static string ToJson(this ClaimsPrincipal principal, JsonSerializerOptions? options = null)
{
return JsonSerializer.Serialize(principal, options ?? DefaultOptions);
}
///
/// 从 JSON 反序列化为 ClaimsPrincipal
///
public static ClaimsPrincipal? FromJson(string json, JsonSerializerOptions? options = null)
{
return JsonSerializer.Deserialize(json, options ?? DefaultOptions);
}
///
/// 将 ClaimsPrincipal 安全地序列化为 JSON(过滤敏感信息)
///
public static string ToJsonSafe(this ClaimsPrincipal principal, HashSet? sensitiveTypes = null)
{
sensitiveTypes ??= new HashSet(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();
}
}
使用扩展方法:
// 序列化
var json = httpContext.User.ToJson();
// 安全序列化(自动过滤敏感信息)
var safeJson = httpContext.User.ToJsonSafe();
// 反序列化
var principal = ClaimsPrincipalExtensions.FromJson(json);
- 实际应用示例
4.1 中间件中传递用户身份
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 分布式缓存
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 GetSessionAsync(string sessionId)
{
var bytes = await _cache.GetAsync(sessionId);
if (bytes == null) return null;
var json = Encoding.UTF8.GetString(bytes);
return ClaimsPrincipalExtensions.FromJson(json);
}
}
- DTO vs Converter:如何选择?
5.1 使用 DTO 的场景
优点:
显式转换,逻辑清晰
可以灵活定制传输的数据结构
更容易进行数据验证和转换
不依赖特定的序列化框架
适用场景:
API 数据传输(需要版本控制和向后兼容)
跨语言/跨平台通信
需要数据脱敏或转换的场景
长期存储的数据格式
示例:
// 为前端提供简化的用户信息
public class UserInfoDto
{
public string UserId { get; set; }
public string Name { get; set; }
public List 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 的场景
优点:
透明序列化,使用更简洁
保持原始类型,无需转换
全局配置一次,处处可用
更好地支持嵌套对象序列化
适用场景:
内部服务通信(同技术栈)
临时缓存和会话存储
调试和日志记录
需要完整保留对象状态
示例:
// 缓存完整的用户身份
public async Task CacheUserPrincipal(string key, ClaimsPrincipal principal)
{
// 直接序列化,无需转换
var json = principal.ToJson();
await _cache.SetStringAsync(key, json, TimeSpan.FromMinutes(30));
}
5.3 混合使用
在实际项目中,两种方式可以共存:
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);
}
}
- 性能对比
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(json);
return deserializedDto!.ToClaimsPrincipal();
}
Benchmark
public ClaimsPrincipal DeserializeWithConverter()
{
var json = JsonSerializer.Serialize(_principal, _options);
return JsonSerializer.Deserialize(json, _options)!;
}
}
// 运行基准测试
// BenchmarkRunner.Run();
预期结果:
Converter 方式通常略快(少一次对象转换)
内存使用相近
两者性能差异在大多数场景下可忽略
- 注意事项
7.1 空值处理
确保 Converter 正确处理 null 值:
public override void Write(Utf8JsonWriter writer, ClaimsPrincipal? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
// ... 正常序列化逻辑
}
7.2 循环引用
虽然 ClaimsPrincipal 结构简单,不会出现循环引用,但在扩展时需注意:
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.IgnoreCycles, // 处理循环引用
Converters = { /* ... */ }
};