使用OpenCvSharp , Emgu.CV 手搓 视觉识别算法 以及 成果展示

切链仆俏自定义 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

  1. 实现自定义 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();

}

}

  1. 使用自定义 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);

  1. 实际应用示例

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);

}

}

  1. 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);

}

}

  1. 性能对比

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 方式通常略快(少一次对象转换)

内存使用相近

两者性能差异在大多数场景下可忽略

  1. 注意事项

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 = { /* ... */ }

};