398 lines
15 KiB
C#
398 lines
15 KiB
C#
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; }
|
||
}
|
||
}
|