|
|
|
|
|
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加密
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|