.Net Web API 参数验签

前言

  在开放的api接口中,我们通过http Post或者Get请求服务器的时候,会面临着许多的安全性问题。为了保证数据在通信时的安全性,我们可以采用TOKEN+参数签名的方式来进行相关验证。

  •   Token(本文使用jwt)来保证访问接口的用户身份合法。这种场景主要用于前端(网页、移动端)的接口访问身份验证方式。前面一篇已经做了介绍:.net core web api授权、鉴权、API保护 - chenxizhaolu - 博客园
  •   用Sign参数签名的方式来防止被篡改和请求的唯一性,是本篇介绍的重点。这种场景主要用户给第三方提供API接口的身份验证。

  本文所有的实现代码可以参考:https://gitee.com/xiaoqingyao/web-app-identity.git

工作流程说明

  1、作为服务的提供方,会在达成合作后为第三方提供一个AppId和AppSecret。

  2、第三方通过这AppId和AppSecret请求某web接口拿到动态token。

  3、第三方在请求其他业务接口时用AppId、时间戳、业务参数key和value按照一定规则拼装字符串、随机数、token 拼接字符串后计算md5值作为signKey,signKey跟appid、时间戳、随机数一起放到Header中。(注:Token不传)

  3、服务器在收到请求时,按照客户端相同的规则计算SignKey,与客户端Header传来的值做比对,如果一致则认为验签通过。

   接下来每一步骤进行详细说明并提供关键代码。

1、AppId和AppSecret的维护

  这个就很简单了,就维护一个数据库表进行维护AppId和AppSecret。新增一个合作伙伴的时候新增一条记录即可。

    

2、获取动态Token

/// <summary>
/// 通过appkey与appSecret获取token
/// </summary>
/// <param name="thirdClient"></param>
/// <returns></returns>
[HttpPost("GetToken")]
[AllowAnonymous]
public async Task<string> GetToken(ThirdClient thirdClient)
{
    return await _thirdClientService.GetToken(thirdClient);
}
public async Task<string> GetToken(ThirdClient thirdClient)
{
    var cacheKey = "thirdClient_" + thirdClient.AppKey;
    if (_memoryCache.TryGetValue(cacheKey, out var cacheToken))
    {
        return cacheToken?.ToString() ?? "";
    }
    var exist = await _appDbContext.ThirdClients.AnyAsync(p => p.AppKey == thirdClient.AppKey && p.AppSecret == thirdClient.AppSecret);
    if (!exist)
    {
        return string.Empty;
    }
    var token = Guid.NewGuid().ToString().Replace("-", "").ToUpper();
    _memoryCache.Set(cacheKey, token, TimeSpan.FromMinutes(10));
    return token;
}

3、客户端请求及添加参数签名、

internal class Program
{
    static async Task Main(string[] args)
    {
        var token = await GetToken();
        var getResult = await Get(token);
        Console.WriteLine("get请求结果:" + getResult);
        Thread.Sleep(1000);
        var postResult = await Post(token);
        Console.WriteLine("post请求结果:" + postResult);
        Thread.Sleep(1000);
        getResult = await Get(token);
        Console.WriteLine("get请求结果:" + getResult);
        Thread.Sleep(1000);
        postResult = await Post(token);
        Console.WriteLine("post请求结果:" + postResult);
        Console.WriteLine("Hello, World!");
    }

    static async Task<string> Post(string token)
    {
        HttpClient client = new HttpClient();
        var nonce = new Random().Next().ToString();
        var appKey = "etcp";
        var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // Unix 时间戳(秒)

        client.DefaultRequestHeaders.Add("AppKey", appKey);
        client.DefaultRequestHeaders.Add("TimeStamp", timeStamp.ToString());
        client.DefaultRequestHeaders.Add("Nonce", nonce);

        IDictionary<string, string> sortedParams = new SortedDictionary<string, string>
        {
            { "id", "1" },
            { "name", "张三" }
        };

        IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();
        StringBuilder query = new StringBuilder();
        while (dem.MoveNext())
        {
            string key = dem.Current.Key;
            string value = dem.Current.Value;
            if (!string.IsNullOrEmpty(key))
            {
                query.Append(key).Append(value);
            }
        }
        string requestDataStr = query.ToString();

        var sign = Md5Helper.ComputeMd5Hash(timeStamp.ToString() + nonce.ToString() + appKey.ToString()
            + token.ToString() + requestDataStr);
        client.DefaultRequestHeaders.Add("Sign", sign);

        var jsonContent = JsonSerializer.Serialize(sortedParams);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

        var response = await client.PostAsync($"http://localhost:5203/SignProtectedAPI/PostApi", content);
        // 确保 HTTP 响应状态是成功的
        response.EnsureSuccessStatusCode();
        // 读取响应内容
        var responseBody = await response.Content.ReadAsStringAsync();
        return responseBody;

    }

    static async Task<string> Get(string token)
    {
        HttpClient client = new HttpClient();
        var nonce = 5;
        var appKey = "etcp";
        var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // Unix 时间戳(秒)

        client.DefaultRequestHeaders.Add("AppKey", appKey);
        client.DefaultRequestHeaders.Add("TimeStamp", timeStamp.ToString());
        client.DefaultRequestHeaders.Add("Nonce", nonce.ToString());

        IDictionary<string, string> sortedParams = new SortedDictionary<string, string>
        {
            { "id", "1" },
            { "name", "张三" }
        };

        IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();
        StringBuilder query = new StringBuilder();
        while (dem.MoveNext())
        {
            string key = dem.Current.Key;
            string value = dem.Current.Value;
            if (!string.IsNullOrEmpty(key))
            {
                query.Append(key).Append(value);
            }
        }
        string requestDataStr = query.ToString();

        var sign = Md5Helper.ComputeMd5Hash(timeStamp.ToString() + nonce.ToString() + appKey.ToString()
            + token.ToString() + requestDataStr);
        client.DefaultRequestHeaders.Add("Sign", sign);

        var response = await client.GetAsync($"http://localhost:5203/SignProtectedAPI/List?{BuildQueryString(sortedParams.ToDictionary())}");
        // 确保 HTTP 响应状态是成功的
        response.EnsureSuccessStatusCode();
        // 读取响应内容
        var responseBody = await response.Content.ReadAsStringAsync();
        return responseBody;

    }

    private static string BuildQueryString(Dictionary<string, string> parameters)
    {
        return string.Join("&", parameters.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
    }

    static async Task<string> GetToken()
    {
        HttpClient client = new HttpClient();
        //获取token
        var jsonContent = "{\"appKey\":\"etcp\", \"appSecret\":\"34853399624948488e1b3cb63ea5e578\"}"; // key 与 secret提前提供
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await client.PostAsync("http://localhost:5203/ThirdClient/GetToken", content);

        // 确保 HTTP 响应状态是成功的
        response.EnsureSuccessStatusCode();
        var token = await response.Content.ReadAsStringAsync();
        return token;
    }
}

4、服务器端验证

  添加并注册一个中间件SignValidateMiddleware。

public class SignValidateMiddleware
{
    private readonly RequestDelegate _next;

    private readonly IServiceScopeFactory _scopeFactory;

    private readonly IMemoryCache _memoryCache;

    public SignValidateMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory, IMemoryCache memoryCache1)
    {
        _next = next;
        _scopeFactory = scopeFactory;
        _memoryCache = memoryCache1;
    }

    public async Task InvokeAsync(HttpContext context,IServiceProvider serviceProvider)
    {
        var requestPath = context.Request.Path;
        var exceptUrls = new string[] { "/User/", "/WeatherForecast/", "/swagger/", "/ThirdClient/" };
        if (requestPath.HasValue && exceptUrls.Any(p => requestPath.Value.IndexOf(p) > -1))
        {
            await _next(context);
            return;
        }
        
        if (!context.Request.Headers.TryGetValue("AppKey", out var appKey))
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签不通过-appKey");
            return;
        }
        if (!context.Request.Headers.TryGetValue("TimeStamp", out var timeStamp) || string.IsNullOrEmpty(timeStamp))
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签不通过-TimeStamp");
            return;
        }
        if (!context.Request.Headers.TryGetValue("Nonce", out var nonce))
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签不通过-Nonce");
            return;
        }
        if (!context.Request.Headers.TryGetValue("Sign", out var sign))
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签不通过-Sign");
            return;
        }

        bool timespanvalidate = double.TryParse(timeStamp.ToString(), out double ts1);
        double ts2 = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        double ts = ts2 - ts1;
        bool flag = ts > 1000 * 30;//30秒内有效
        if (flag || !timespanvalidate)
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签不通过-timespanvalidate");
            return;
        }

        var cacheKey = "thirdClient_" + appKey;
        if (!_memoryCache.TryGetValue(cacheKey, out var token))
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("token验证不通过,可能已经过期,请重新获取token再试-token");
            return;
        }
        var _userContext = serviceProvider.GetRequiredService<UserDbContext>()
                  ?? throw new ArgumentNullException("未获取到_userContext");
        var thirdClient = _userContext.ThirdClients.FirstOrDefault(p => p.AppKey == appKey.ToString());
        if (thirdClient == null)
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签不通过-AppKey");
            return;
        }

        IDictionary<string, string> parameters;
        if (context.Request.Method.Equals(HttpMethod.Get.ToString(), StringComparison.CurrentCultureIgnoreCase))
        {
            parameters = new Dictionary<string, string>();
            foreach (var kvp in context.Request.Query)
            {
                parameters.Add(kvp.Key, kvp.Value.ToString());
            }
        }
        else if (context.Request.Method.Equals(HttpMethod.Post.ToString(), StringComparison.CurrentCultureIgnoreCase))
        {
            var requestData = await new StreamReader(context.Request.Body).ReadToEndAsync();
            parameters = JsonConvert.DeserializeObject<Dictionary<string, string>>(requestData ?? "{}") ?? [];

            // 将修改后的请求体重新写入HttpContext,防止action拿不到数据报错
            if (!string.IsNullOrEmpty(requestData))
            {
                byte[] requestBodyBytes = Encoding.UTF8.GetBytes(requestData);
                context.Request.Body = new MemoryStream(requestBodyBytes);
            }                
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("不能识别的请求方法");
            return;
        }

        string requestDataStr = GetSortedRequestDataStr(parameters); //请求参数字符串拼接
        string computedSign = Md5Helper.ComputeMd5Hash(timeStamp.ToString() + nonce.ToString() + appKey.ToString() + token.ToString() + requestDataStr);
        if (!sign.ToString().Equals(computedSign, StringComparison.CurrentCultureIgnoreCase))
        {
            context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await context.Response.WriteAsync("验签验证失败");
            return;
        }
        await _next(context);

    }
    public string GetSortedRequestDataStr(IDictionary<string, string> parameters)
    {
        IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters);
        IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();

        StringBuilder query = new StringBuilder();
        while (dem.MoveNext())
        {
            string key = dem.Current.Key;
            string value = dem.Current.Value;
            if (!string.IsNullOrEmpty(key))
            {
                query.Append(key).Append(value);
            }
        }
        return query.ToString();
    }
}

 参考:ASP.NET WebApi TOKEN+签名认证_tokensign.getvalue()-CSDN博客

posted @ 2024-10-25 15:38  chenxizhaolu  阅读(16)  评论(0编辑  收藏  举报