xiangyixiangqin/server/src/XiangYi.Infrastructure/RealName/TencentRealNameProvider.cs
2026-01-24 20:20:09 +08:00

398 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace XiangYi.Infrastructure.RealName;
/// <summary>
/// 腾讯云实名认证服务实现
/// 使用腾讯云人脸核身API
/// </summary>
public class TencentRealNameProvider : IRealNameProvider
{
private readonly TencentRealNameOptions _options;
private readonly ILogger<TencentRealNameProvider> _logger;
private readonly HttpClient _httpClient;
private const string ServiceHost = "faceid.tencentcloudapi.com";
private const string ServiceName = "faceid";
private const string ApiVersion = "2018-03-01";
public TencentRealNameProvider(
IOptions<RealNameOptions> options,
IHttpClientFactory httpClientFactory,
ILogger<TencentRealNameProvider> logger)
{
_options = options.Value.Tencent;
_logger = logger;
_httpClient = httpClientFactory.CreateClient("TencentRealName");
}
public async Task<RealNameResult> VerifyIdCardAsync(string name, string idCard)
{
try
{
var requestBody = new
{
IdCard = idCard,
Name = name
};
var response = await CallApiAsync("IdCardVerification", requestBody);
if (response?.Response?.Result == "0")
{
_logger.LogInformation("身份证二要素验证通过: {IdCard}", MaskIdCard(idCard));
return RealNameResult.Success(response.Response.RequestId);
}
var errorMsg = response?.Response?.Description ?? "验证失败";
_logger.LogWarning("身份证二要素验证失败: {IdCard}, Error: {Error}",
MaskIdCard(idCard), errorMsg);
return RealNameResult.Fail(
response?.Response?.Result ?? "UNKNOWN",
errorMsg,
response?.Response?.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "身份证二要素验证异常: {IdCard}", MaskIdCard(idCard));
return RealNameResult.Fail("EXCEPTION", ex.Message);
}
}
public async Task<RealNameResult> VerifyWithPhotoAsync(string name, string idCard, string photoBase64)
{
try
{
var requestBody = new
{
IdCard = idCard,
Name = name,
ImageBase64 = photoBase64
};
var response = await CallApiAsync("IdCardOCRVerification", requestBody);
if (response?.Response?.Result == "0")
{
var score = decimal.TryParse(response.Response.Sim, out var s) ? s : 0;
if (score >= _options.SimilarityThreshold)
{
_logger.LogInformation("身份证三要素验证通过: {IdCard}, Score: {Score}",
MaskIdCard(idCard), score);
return RealNameResult.Success(response.Response.RequestId, score);
}
_logger.LogWarning("身份证三要素验证相似度不足: {IdCard}, Score: {Score}, Threshold: {Threshold}",
MaskIdCard(idCard), score, _options.SimilarityThreshold);
return RealNameResult.Fail(
"LOW_SIMILARITY",
$"人脸相似度不足,当前: {score},要求: {_options.SimilarityThreshold}",
response.Response.RequestId);
}
var errorMsg = response?.Response?.Description ?? "验证失败";
_logger.LogWarning("身份证三要素验证失败: {IdCard}, Error: {Error}",
MaskIdCard(idCard), errorMsg);
return RealNameResult.Fail(
response?.Response?.Result ?? "UNKNOWN",
errorMsg,
response?.Response?.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "身份证三要素验证异常: {IdCard}", MaskIdCard(idCard));
return RealNameResult.Fail("EXCEPTION", ex.Message);
}
}
/// <summary>
/// 身份证OCR识别正面
/// </summary>
public async Task<IdCardOcrResult> OcrIdCardFrontAsync(string imageBase64)
{
try
{
var requestBody = new
{
ImageBase64 = imageBase64,
CardSide = "FRONT"
};
var response = await CallOcrApiAsync("IDCardOCR", requestBody);
if (response?.Response?.Error != null)
{
_logger.LogWarning("身份证正面OCR识别失败: {Error}", response.Response.Error.Message);
return IdCardOcrResult.Fail(
response.Response.Error.Code ?? "UNKNOWN",
response.Response.Error.Message ?? "识别失败",
response.Response.RequestId);
}
if (string.IsNullOrEmpty(response?.Response?.Name) || string.IsNullOrEmpty(response?.Response?.IdNum))
{
_logger.LogWarning("身份证正面OCR识别结果为空");
return IdCardOcrResult.Fail("EMPTY_RESULT", "未能识别到身份证信息,请重新拍照");
}
_logger.LogInformation("身份证正面OCR识别成功: {Name}", MaskName(response.Response.Name));
return IdCardOcrResult.SuccessFront(
response.Response.Name,
response.Response.IdNum,
response.Response.Sex,
response.Response.Nation,
response.Response.Birth,
response.Response.Address,
response.Response.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "身份证正面OCR识别异常");
return IdCardOcrResult.Fail("EXCEPTION", ex.Message);
}
}
/// <summary>
/// 身份证OCR识别反面
/// </summary>
public async Task<IdCardOcrResult> OcrIdCardBackAsync(string imageBase64)
{
try
{
var requestBody = new
{
ImageBase64 = imageBase64,
CardSide = "BACK"
};
var response = await CallOcrApiAsync("IDCardOCR", requestBody);
if (response?.Response?.Error != null)
{
_logger.LogWarning("身份证反面OCR识别失败: {Error}", response.Response.Error.Message);
return IdCardOcrResult.Fail(
response.Response.Error.Code ?? "UNKNOWN",
response.Response.Error.Message ?? "识别失败",
response.Response.RequestId);
}
if (string.IsNullOrEmpty(response?.Response?.Authority))
{
_logger.LogWarning("身份证反面OCR识别结果为空");
return IdCardOcrResult.Fail("EMPTY_RESULT", "未能识别到身份证信息,请重新拍照");
}
_logger.LogInformation("身份证反面OCR识别成功");
return IdCardOcrResult.SuccessBack(
response.Response.Authority,
response.Response.ValidDate ?? "",
response.Response.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "身份证反面OCR识别异常");
return IdCardOcrResult.Fail("EXCEPTION", ex.Message);
}
}
private async Task<TencentOcrApiResponse?> CallOcrApiAsync(string action, object requestBody)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var date = DateTime.UtcNow.ToString("yyyy-MM-dd");
var payload = JsonSerializer.Serialize(requestBody);
// OCR使用不同的服务域名
var ocrHost = "ocr.tencentcloudapi.com";
var ocrService = "ocr";
var authorization = GenerateAuthorizationForOcr(action, timestamp, date, payload, ocrHost, ocrService);
var request = new HttpRequestMessage(HttpMethod.Post, $"https://{ocrHost}/")
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
};
request.Headers.Add("Authorization", authorization);
request.Headers.Add("X-TC-Action", action);
request.Headers.Add("X-TC-Version", "2018-11-19"); // OCR API版本
request.Headers.Add("X-TC-Timestamp", timestamp.ToString());
request.Headers.Add("X-TC-Region", _options.Region);
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TencentOcrApiResponse>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
private string GenerateAuthorizationForOcr(string action, long timestamp, string date, string payload, string host, string service)
{
var algorithm = "TC3-HMAC-SHA256";
var httpRequestMethod = "POST";
var canonicalUri = "/";
var canonicalQueryString = "";
var canonicalHeaders = $"content-type:application/json; charset=utf-8\nhost:{host}\n";
var signedHeaders = "content-type;host";
var hashedRequestPayload = Sha256Hex(payload);
var canonicalRequest = $"{httpRequestMethod}\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
var credentialScope = $"{date}/{service}/tc3_request";
var hashedCanonicalRequest = Sha256Hex(canonicalRequest);
var stringToSign = $"{algorithm}\n{timestamp}\n{credentialScope}\n{hashedCanonicalRequest}";
var secretDate = HmacSha256($"TC3{_options.SecretKey}", date);
var secretService = HmacSha256(secretDate, service);
var secretSigning = HmacSha256(secretService, "tc3_request");
var signature = HmacSha256Hex(secretSigning, stringToSign);
return $"{algorithm} Credential={_options.SecretId}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}";
}
private static string MaskName(string name)
{
if (string.IsNullOrEmpty(name) || name.Length < 2)
return "***";
return $"{name[0]}**";
}
private async Task<TencentApiResponse?> CallApiAsync(string action, object requestBody)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var date = DateTime.UtcNow.ToString("yyyy-MM-dd");
var payload = JsonSerializer.Serialize(requestBody);
var authorization = GenerateAuthorization(action, timestamp, date, payload);
var request = new HttpRequestMessage(HttpMethod.Post, $"https://{ServiceHost}/")
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
};
request.Headers.Add("Authorization", authorization);
request.Headers.Add("X-TC-Action", action);
request.Headers.Add("X-TC-Version", ApiVersion);
request.Headers.Add("X-TC-Timestamp", timestamp.ToString());
request.Headers.Add("X-TC-Region", _options.Region);
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TencentApiResponse>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
private string GenerateAuthorization(string action, long timestamp, string date, string payload)
{
var algorithm = "TC3-HMAC-SHA256";
var httpRequestMethod = "POST";
var canonicalUri = "/";
var canonicalQueryString = "";
var canonicalHeaders = $"content-type:application/json; charset=utf-8\nhost:{ServiceHost}\n";
var signedHeaders = "content-type;host";
var hashedRequestPayload = Sha256Hex(payload);
var canonicalRequest = $"{httpRequestMethod}\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
var credentialScope = $"{date}/{ServiceName}/tc3_request";
var hashedCanonicalRequest = Sha256Hex(canonicalRequest);
var stringToSign = $"{algorithm}\n{timestamp}\n{credentialScope}\n{hashedCanonicalRequest}";
var secretDate = HmacSha256($"TC3{_options.SecretKey}", date);
var secretService = HmacSha256(secretDate, ServiceName);
var secretSigning = HmacSha256(secretService, "tc3_request");
var signature = HmacSha256Hex(secretSigning, stringToSign);
return $"{algorithm} Credential={_options.SecretId}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}";
}
private static string Sha256Hex(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLower();
}
private static byte[] HmacSha256(string key, string data)
{
return HmacSha256(Encoding.UTF8.GetBytes(key), data);
}
private static byte[] HmacSha256(byte[] key, string data)
{
using var hmac = new HMACSHA256(key);
return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
}
private static string HmacSha256Hex(byte[] key, string data)
{
return Convert.ToHexString(HmacSha256(key, data)).ToLower();
}
private static string MaskIdCard(string idCard)
{
if (string.IsNullOrEmpty(idCard) || idCard.Length < 10)
return "***";
return $"{idCard[..4]}**********{idCard[^4..]}";
}
private class TencentApiResponse
{
public TencentApiResponseData? Response { get; set; }
}
private class TencentApiResponseData
{
public string? Result { get; set; }
public string? Description { get; set; }
public string? Sim { get; set; }
public string? RequestId { get; set; }
public TencentApiError? Error { get; set; }
}
private class TencentApiError
{
public string? Code { get; set; }
public string? Message { get; set; }
}
/// <summary>
/// 腾讯云OCR API响应
/// </summary>
private class TencentOcrApiResponse
{
public TencentOcrResponseData? Response { get; set; }
}
private class TencentOcrResponseData
{
// 正面信息
public string? Name { get; set; }
public string? Sex { get; set; }
public string? Nation { get; set; }
public string? Birth { get; set; }
public string? Address { get; set; }
public string? IdNum { get; set; }
// 反面信息
public string? Authority { get; set; }
public string? ValidDate { get; set; }
public string? RequestId { get; set; }
public TencentApiError? Error { get; set; }
}
}