You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
15 KiB
375 lines
15 KiB
using Common.Shared.Application.DaHua; |
|
using Microsoft.Extensions.Configuration; |
|
using Microsoft.Extensions.Logging; |
|
using Org.BouncyCastle.Crypto.Parameters; |
|
using Org.BouncyCastle.Security; |
|
using System.Net.Http.Json; |
|
using System.Security.Cryptography; |
|
using System.Text.Json; |
|
|
|
namespace Common.Shared.DomainService |
|
{ |
|
/// <summary> |
|
/// 获取大华icc平台的token服务 |
|
/// </summary> |
|
public class TokenProviderService : ITokenProviderService |
|
{ |
|
private readonly IConfiguration _configuration; |
|
private readonly ILogger<TokenProviderService> _logger; |
|
|
|
public TokenProviderService(IConfiguration configuration, ILogger<TokenProviderService> logger) |
|
{ |
|
_configuration = configuration; |
|
_logger = logger; |
|
} |
|
|
|
/// <summary> |
|
/// 开发测试的时候,忽略证书 |
|
/// </summary> |
|
private static readonly HttpClient _http = new(new HttpClientHandler |
|
{ |
|
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator |
|
}); |
|
|
|
public async Task<string> GetTokenAsync(string clientId) |
|
{ |
|
if (TokenCache.TokenMap.TryGetValue(clientId, out var tokenEntry) |
|
&& tokenEntry.ExpireAt > DateTimeOffset.UtcNow.AddMinutes(5)) |
|
{ |
|
return tokenEntry.AccessToken!; |
|
} |
|
|
|
var tokenLock = TokenLockProvider.GetLock(clientId); |
|
await tokenLock.WaitAsync(); |
|
try |
|
{ |
|
// 加锁后再次检查,防止重复刷新 |
|
if (TokenCache.TokenMap.TryGetValue(clientId, out tokenEntry) |
|
&& tokenEntry.ExpireAt > DateTimeOffset.UtcNow.AddMinutes(5)) |
|
{ |
|
return tokenEntry.AccessToken!; |
|
} |
|
|
|
var refreshed = await TryRefreshOrLoginAsync(clientId, tokenEntry); |
|
|
|
return refreshed.AccessToken; |
|
} |
|
finally |
|
{ |
|
tokenLock.Release(); |
|
} |
|
} |
|
|
|
private async Task<TokenEntry> TryRefreshOrLoginAsync(string clientId, TokenEntry? current) |
|
{ |
|
try |
|
{ |
|
TokenEntry refreshed; |
|
|
|
if (current?.AccessToken is { } refreshToken) |
|
{ |
|
var dto = new RefreshTokenReqDto |
|
{ |
|
ClientId = clientId, |
|
ClientSecret = _configuration["DahuaAuth:ClientSecret"]!, |
|
GrantType = "refresh_token", |
|
//刷新要求去掉 Bearer 前缀 |
|
RefreshToken = refreshToken.Replace("Bearer ", string.Empty) |
|
}; |
|
|
|
var result = await RefreshToken(dto); |
|
|
|
if (result?.Data != null && result.Data.AccessToken != "" && result.Data.AccessToken != null) |
|
{ |
|
refreshed = new TokenEntry |
|
{ |
|
AccessToken = string.Concat(result.Data.TokenType, " ", result.Data.AccessToken), |
|
|
|
ExpireAt = DateTimeOffset.UtcNow.AddSeconds(result.Data.ExpiresIn) |
|
}; |
|
_logger.LogInformation("Refresh 成功: {ClientId}", clientId); |
|
} |
|
else |
|
{ |
|
_logger.LogWarning("RefreshToken 失败,尝试重新登录"); |
|
refreshed = await GetDaHToken(); |
|
} |
|
} |
|
else |
|
{ |
|
refreshed = await GetDaHToken(); |
|
} |
|
if (refreshed != null && refreshed.AccessToken != "") |
|
{ |
|
// 更新缓存 |
|
TokenCache.TokenMap[clientId] = refreshed; |
|
} |
|
|
|
return refreshed; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogError(ex, "获取 token 异常:{ClientId}", clientId); |
|
return new TokenEntry |
|
{ |
|
AccessToken = string.Empty, |
|
ExpireAt = DateTimeOffset.UtcNow.AddMinutes(1) |
|
}; |
|
} |
|
} |
|
|
|
private async Task<TokenEntry> GetDaHToken() |
|
{ |
|
//1. 获取公钥 |
|
DaHApiResult<PublicKeyDto> publicKeyResult = await GetPublicKey(); |
|
if (publicKeyResult.Success == false) |
|
{ |
|
return new TokenEntry |
|
{ |
|
AccessToken = string.Empty, |
|
ExpireAt = DateTimeOffset.UtcNow.AddMinutes(1) |
|
}; |
|
} |
|
LoginRequestDto dto = new(); |
|
//2. 鉴权 |
|
dto.PublicKey = publicKeyResult.Data!.PublicKey; |
|
dto.ClientId = _configuration["DahuaAuth:ClientId"]!; |
|
dto.ClientSecret = _configuration["DahuaAuth:ClientSecret"]!; |
|
dto.Password = _configuration["DahuaAuth:Password"]!; |
|
dto.Username = _configuration["DahuaAuth:Username"]!; |
|
|
|
DaHApiResult<LoginResDto> loginResult = await GetToken(dto); |
|
|
|
TokenEntry refreshed = new() |
|
{ |
|
AccessToken = loginResult.Data!.AccessToken, |
|
ExpireAt = DateTimeOffset.UtcNow.AddSeconds(120) |
|
}; |
|
return refreshed; |
|
} |
|
|
|
/// <summary> |
|
/// 刷新token,2个小时过期的 |
|
/// </summary> |
|
/// <param name="dto"></param> |
|
/// <returns></returns> |
|
/// <exception cref="ArgumentNullException"></exception> |
|
/// <exception cref="ArgumentException"></exception> |
|
/// <exception cref="InvalidOperationException"></exception> |
|
private async Task<DaHApiResult<TokenResDto>> RefreshToken(RefreshTokenReqDto dto) |
|
{ |
|
DaHApiResult<TokenResDto> result = new DaHApiResult<TokenResDto>() { Success = true, Code = "0" }; |
|
|
|
if (string.IsNullOrWhiteSpace(dto.RefreshToken)) |
|
{ |
|
result.Success = false; |
|
result.Code = "1005"; |
|
result.Msg = "刷新令牌不能为空"; |
|
_logger.LogWarning("刷新大华令牌失败,刷新令牌不能为空"); |
|
return result; |
|
} |
|
|
|
try |
|
{ |
|
var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/extend/refresh/token"; |
|
|
|
using var resp = await _http.PostAsJsonAsync(url, dto); |
|
resp.EnsureSuccessStatusCode(); |
|
|
|
result = await resp.Content.ReadFromJsonAsync<DaHApiResult<TokenResDto>>(); |
|
|
|
if (!result.Success || result.Code != "0") |
|
{ |
|
result.Success = false; |
|
result.Code = "1006"; |
|
result.Msg = "刷新大华令牌失败"; |
|
_logger.LogWarning("刷新大华令牌失败,返回结果:{Result}", result); |
|
} |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogError(ex, "刷新大华令牌出错"); |
|
result.Success = false; |
|
result.Code = "1006"; |
|
result.Msg = "刷新大华令牌失败"; |
|
} |
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// 获取公钥 |
|
/// </summary> |
|
/// <returns></returns> |
|
/// <exception cref="NotImplementedException"></exception> |
|
private async Task<DaHApiResult<PublicKeyDto>> GetPublicKey() |
|
{ |
|
DaHApiResult<PublicKeyDto> result = new() { Success = true, Code = "0", Data = new PublicKeyDto() { PublicKey = "" } }; |
|
try |
|
{ |
|
var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/public-key"; |
|
|
|
using var resp = await _http.GetAsync(url); |
|
resp.EnsureSuccessStatusCode(); |
|
var json = await resp.Content.ReadAsStringAsync(); |
|
var envelope = JsonSerializer.Deserialize<DaHApiResult<PublicKeyDto>>(json, new JsonSerializerOptions |
|
{ |
|
PropertyNameCaseInsensitive = true |
|
}); |
|
|
|
if (envelope?.Data?.PublicKey is null or { Length: 0 }) |
|
{ |
|
_logger.LogWarning("获取大华公钥失败,返回结果:{Result}", json); |
|
result.Success = false; |
|
result.Code = "1001"; |
|
result.Msg = "获取大华公钥失败"; |
|
|
|
return result; |
|
} |
|
|
|
result.Data.PublicKey = envelope.Data.PublicKey; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogWarning(ex, "大华平台获取公钥出错"); |
|
result.Success = false; |
|
result.Code = "1001"; |
|
result.Msg = "获取大华公钥失败"; |
|
} |
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// 获取token |
|
/// </summary> |
|
/// <param name="dto"></param> |
|
/// <returns></returns> |
|
private async Task<DaHApiResult<LoginResDto>> GetToken(LoginRequestDto dto) |
|
{ |
|
DaHApiResult<LoginResDto> result = new() { Success = true, Code = "0", Data = new LoginResDto { } }; |
|
if (dto is null) |
|
{ |
|
result.Success = false; |
|
|
|
result.Code = "1002"; |
|
result.Msg = "请求参数不能为空"; |
|
_logger.LogWarning("获取大华登录令牌失败,参数不能为空"); |
|
return result; |
|
} |
|
if (string.IsNullOrWhiteSpace(dto.Password)) |
|
{ |
|
result.Success = false; |
|
result.Code = "1003"; |
|
result.Msg = "密码不能为空"; |
|
_logger.LogWarning("获取大华登录令牌失败,密码不能为空"); |
|
return result; |
|
} |
|
try |
|
{ |
|
var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/extend/token"; |
|
//必须加密 |
|
dto.Password = EncryptByPublicKey(dto.Password, dto.PublicKey!); |
|
using var resp = await _http.PostAsJsonAsync(url, dto); |
|
resp.EnsureSuccessStatusCode(); |
|
|
|
var tokenInfo = await resp.Content.ReadFromJsonAsync<DaHApiResult<LoginResDto>>(); |
|
|
|
if (tokenInfo == null || !result.Success || result.Code != "0") |
|
{ |
|
result.Success = false; |
|
result.Code = "1004"; |
|
result.Msg = "获取大华登录令牌失败"; |
|
_logger.LogWarning("获取大华登录令牌失败,返回结果:{Result}", result); |
|
} |
|
result = tokenInfo!; |
|
//固定的拼接方式 |
|
result.Data!.AccessToken = string.Concat(tokenInfo?.Data!.TokenType, " ", tokenInfo?.Data!.AccessToken); |
|
|
|
TokenEntry refreshed = new TokenEntry |
|
{ |
|
AccessToken = string.Concat(result!.Data.TokenType, " ", result.Data.AccessToken), |
|
|
|
ExpireAt = DateTimeOffset.UtcNow.AddSeconds(result.Data.ExpiresIn) |
|
}; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogError(ex, "获取大华登录令牌出错"); |
|
result.Success = false; |
|
result.Code = "1004"; |
|
result.Msg = "获取大华登录令牌失败"; |
|
} |
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// 判断token是否有效 |
|
/// </summary> |
|
/// <param name="token"></param> |
|
/// <returns></returns> |
|
public bool IsTokenValid(string token) |
|
{ |
|
// 避免 NullReferenceException |
|
if (string.IsNullOrWhiteSpace(token)) |
|
return true; |
|
|
|
// 统一写法,后续改条件只改这里 |
|
return token.Length < 10; |
|
} |
|
|
|
#region RES加密 |
|
|
|
private static string EncryptByPublicKey(string context, string publicKey) |
|
{ |
|
RSACryptoServiceProvider rsa = new(); |
|
|
|
rsa.ImportParameters(FromXmlStringExtensions(ConvertToXmlPublicJavaKey(publicKey))); |
|
byte[] byteText = System.Text.Encoding.UTF8.GetBytes(context); |
|
byte[] byteEntry = rsa.Encrypt(byteText, false); |
|
return Convert.ToBase64String(byteEntry); |
|
} |
|
|
|
private static RSAParameters FromXmlStringExtensions(string xmlString) |
|
{ |
|
RSAParameters parameters = new RSAParameters(); |
|
|
|
System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument(); |
|
xmlDoc.LoadXml(xmlString); |
|
|
|
if (xmlDoc.DocumentElement!.Name.Equals("RSAKeyValue")) |
|
{ |
|
foreach (System.Xml.XmlNode node in xmlDoc.DocumentElement.ChildNodes) |
|
{ |
|
switch (node.Name) |
|
{ |
|
case "Modulus": parameters.Modulus = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "Exponent": parameters.Exponent = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "P": parameters.P = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "Q": parameters.Q = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "DP": parameters.DP = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "DQ": parameters.DQ = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "InverseQ": parameters.InverseQ = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
case "D": parameters.D = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
throw new Exception("Invalid XML RSA key."); |
|
} |
|
|
|
return parameters; |
|
} |
|
|
|
private static string ConvertToXmlPublicJavaKey(string publicJavaKey) |
|
{ |
|
RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicJavaKey)); |
|
string xmlpublicKey = string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>", |
|
Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()), |
|
Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned())); |
|
Console.WriteLine(xmlpublicKey); |
|
return xmlpublicKey; |
|
} |
|
|
|
#endregion RES加密 |
|
} |
|
} |