using ChouBox.Code.AppExtend;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace ChouBox.Code.MiddlewareExtend
{
///
/// 请求签名验证中间件
///
public class SignatureVerifyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
private readonly IConnectionMultiplexer _redisConnection;
///
/// Redis键前缀
///
private const string REDIS_KEY_PREFIX = "api_nonce:";
///
/// Nonce过期时间(秒)
///
private const int NONCE_EXPIRE_TIME = 600; // 10分钟
///
/// 时间戳允许的误差(秒)
///
private const int TIMESTAMP_TOLERANCE = 60; // 1分钟
// 修改构造函数
public SignatureVerifyMiddleware(
RequestDelegate next,
IConfiguration configuration,
ILogger logger,
IConnectionMultiplexer redisConnection)
{
_next = next;
_configuration = configuration;
_logger = logger;
_redisConnection = redisConnection;
}
// 添加属性获取Redis数据库
private IDatabase Redis => _redisConnection.GetDatabase();
///
/// 处理请求签名验证
///
public async Task InvokeAsync(HttpContext context)
{
// 获取当前请求路径
var path = context.Request.Path.Value?.TrimStart('/') ?? string.Empty;
// 检查是否在白名单内
if (IsWhitelistedPath(path, context))
{
await _next(context);
return;
}
// 根据请求方法进行签名验证
var method = context.Request.Method.ToUpper();
Dictionary parameters = new Dictionary();
if (method == "GET")
{
foreach (var item in context.Request.Query)
{
parameters[item.Key] = item.Value.ToString();
}
}
else if (method == "POST")
{
// 读取表单数据
if (context.Request.HasFormContentType)
{
foreach (var item in context.Request.Form)
{
parameters[item.Key] = item.Value.ToString();
}
}
else
{
// 读取JSON数据
context.Request.EnableBuffering();
using var reader = new System.IO.StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true);
var bodyText = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
if (!string.IsNullOrEmpty(bodyText))
{
try
{
var jsonData = JsonConvert.DeserializeObject>(bodyText);
if (jsonData != null)
{
foreach (var item in jsonData)
{
parameters[item.Key] = item.Value?.ToString() ?? string.Empty;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "解析请求JSON失败");
await Error(context, "无效的请求格式");
return;
}
}
}
}
try
{
await VerifySignature(context, parameters);
}
catch (Exception ex)
{
_logger.LogError(ex, "签名验证失败");
await Error(context, ex.Message);
return;
}
// 继续执行下一个中间件
await _next(context);
}
///
/// 检查请求路径是否在白名单中
///
private bool IsWhitelistedPath(string path, HttpContext context)
{
// 检查是否有内部标识
if (context.Request.Query.TryGetValue("is_test", out var isTest) && isTest == "true")
{
return true;
}
// 检查IP白名单
var ipWhitelist = GetIpWhitelist();
var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
if (ipWhitelist.Contains(clientIp))
{
return true;
}
// 获取白名单路径
var whitelistPaths = GetWhitelistPaths();
// 检查路径是否在白名单内
foreach (var whitePath in whitelistPaths)
{
if (PathMatch(whitePath, path))
{
return true;
}
}
return false;
}
///
/// 验证请求签名
///
private async Task VerifySignature(HttpContext context, Dictionary parameters)
{
// 检查是否有必要的签名参数
if (!parameters.ContainsKey("timestamp") || !parameters.ContainsKey("sign") || !parameters.ContainsKey("nonce"))
{
throw new Exception("缺少必要的签名参数");
}
// 检查时间戳是否在允许范围内(1分钟误差)
if (!long.TryParse(parameters["timestamp"], out long timestamp))
{
throw new Exception("无效的时间戳格式");
}
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(now - timestamp) > TIMESTAMP_TOLERANCE)
{
throw new Exception("请求时间戳超出允许范围");
}
// 检查nonce是否被使用过(防重放攻击)
var nonce = parameters["nonce"];
var nonceKey = REDIS_KEY_PREFIX + nonce;
var existingNonce = await Redis.KeyExistsAsync(nonceKey);
if (existingNonce)
{
throw new Exception("无效的请求(nonce已被使用)");
}
// 记录nonce到Redis,有效期10分钟(足够覆盖时间戳可接受的误差范围)
await Redis.StringSetAsync(
nonceKey,
"1",
TimeSpan.FromSeconds(timestamp)
);
// 从请求中获取签名
var requestSign = parameters["sign"];
// 拷贝参数,移除不需要的参数
var signParams = new Dictionary(parameters);
if (signParams.ContainsKey("s"))
signParams.Remove("s"); // 移除URL参数
signParams.Remove("sign"); // 移除签名参数
// 按照键名对参数进行排序
var sortedParams = signParams.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
// 组合参数为字符串
var signStr = new StringBuilder();
foreach (var param in sortedParams)
{
var value = param.Value;
// 处理复杂类型的参数
if (value.StartsWith("{") || value.StartsWith("["))
{
try
{
// 尝试解析为JSON,格式化处理
var jsonObj = JsonConvert.DeserializeObject(value);
value = JsonConvert.SerializeObject(jsonObj, Formatting.None);
}
catch
{
// 如果解析失败,使用原始值
}
}
signStr.Append(param.Key).Append("=").Append(value).Append("&");
}
// 获取当前请求的域名和时间戳,组合为密钥
var host = context.Request.Host.Value;
var appSecret = host + timestamp;
// 添加密钥
if (signStr.Length > 0)
{
signStr.Length--; // 删除末尾的&符号
}
signStr.Append(appSecret);
// 生成本地签名(使用MD5签名算法)
var localSign = GetMd5Hash(signStr.ToString());
// 比对签名
if (requestSign != localSign)
{
throw new Exception("签名验证失败");
}
}
///
/// 返回错误信息
///
private async Task Error(HttpContext context, string message, int code = 0)
{
var result = new
{
status = code,
msg = message,
data = (object)null
};
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(JsonConvert.SerializeObject(result));
}
///
/// 获取路径白名单
///
private IEnumerable GetWhitelistPaths()
{
// 1. 默认白名单路径(如支付回调通知等)
var defaultWhitelist = new List
{
"notify/*", // 支付回调等通知
"health", // 健康检查
"debug", // 调试接口
"generate_urllinks",
"webhook/*", // webhook路径
"internal/*", // 内部接口
};
// 2. 从配置文件中获取白名单路径
try
{
var configWhitelist = _configuration.GetSection("Api:WhitelistPaths").Get>();
if (configWhitelist != null && configWhitelist.Any())
{
return defaultWhitelist.Concat(configWhitelist);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取API白名单路径配置失败");
}
return defaultWhitelist;
}
///
/// 获取IP白名单
///
private IEnumerable GetIpWhitelist()
{
// 默认IP白名单
var defaultIpWhitelist = new List
{
"127.0.0.1", // 本地回环地址
"::1", // IPv6本地回环地址
};
// 从配置文件中获取IP白名单
try
{
var configIpWhitelist = _configuration.GetSection("Api:IpWhitelist").Get>();
if (configIpWhitelist != null && configIpWhitelist.Any())
{
return defaultIpWhitelist.Concat(configIpWhitelist);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取API白名单IP配置失败");
}
return defaultIpWhitelist;
}
///
/// 路径匹配检查
///
private bool PathMatch(string pattern, string path)
{
// 完全匹配
if (pattern == path)
{
return true;
}
// 通配符匹配 (例如: 'notify/*')
if (pattern.Contains("*"))
{
var regex = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(path, regex, RegexOptions.IgnoreCase);
}
return false;
}
///
/// 计算MD5哈希
///
private string GetMd5Hash(string input)
{
using (var md5 = MD5.Create())
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
var sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("x2"));
}
return sb.ToString();
}
}
}
}