乐趣区

关于后端:WebAPI公开接口请求签名验证

前言

当初的零碎后端开发的时候, 会公开很多 API 接口
对于要登录认证后能力拜访的接口, 这样的申请验证就由身份认证模块实现
然而也有些接口是对外公开的, 没有身份认证的接口
咱们怎么保障接口的申请是非法的, 无效的.
这样咱们个别就是对申请的合法性做签名验证.

实现原理

为保障接口平安,每次申请必带以下 header

| header 名 | 类型 | 形容 |
| AppId | string | 利用 Id |
| Ticks | string | 工夫戳为 1970 年 1 月 1 日到当初工夫的毫秒数(UTC 工夫)|
| RequestId | string | GUID 字符串, 作为申请惟一标记, 避免反复申请 |
| Sign| string | 签名, 签名算法如下 |

拼接字符串 ”{AppId}{Ticks}{RequestId}{AppSecret}”
把拼接后的字符串计算 MD5 值, 此 MD5 值为申请 Header 的 Sign 参数传入
后端把对应 APP 配置好(AppId,AppSecret), 并提供给客户端

后端验证实现

验证 AppId
先验证 AppId 是不是有, 没有就间接返回失败
如果有的话, 就去缓存里取 AppID 对应的配置 (如果缓存里没有, 就去配置文件里取)
如果没有对应 AppId 的配置, 阐明不是正确的申请, 返回失败

验 model.AppId = context.Request.Headers[“AppId”];

        if (String.IsNullOrEmpty(model.AppId))
        {await this.ResponseValidFailedAsync(context, 501);
            return;
        }
        var cacheSvc = context.RequestServices.GetRequiredService<IMemoryCache>();
        var cacheAppIdKey = $"RequestValidSign:APPID:{model.AppId}";
        var curConfig = cacheSvc.GetOrCreate<AppConfigModel>(cacheAppIdKey, (e) =>
        {e.SlidingExpiration = TimeSpan.FromHours(1);
            var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
            var listAppConfig = configuration.GetSection(AppConfigModel.ConfigSectionKey).Get<AppConfigModel[]>();
            return listAppConfig.SingleOrDefault(x => x.AppId == model.AppId);
        });
        if (curConfig == null)
        {await this.ResponseValidFailedAsync(context, 502);
            return;
     }

工夫戳验证

验证工夫戳是不是有在申请头里传过来,没有就返回失败
验证工夫戳与以后工夫比拟,如果不在过期工夫 (5 分钟) 之内的申请,就返回失败
工夫戳为 1970 年 1 月 1 日到当初工夫的毫秒数(UTC 工夫)

            var ticksString = context.Request.Headers["Ticks"].ToString();
            if (String.IsNullOrEmpty(ticksString))
            {await this.ResponseValidFailedAsync(context, 503);
                return;
            }
            model.Ticks = long.Parse(context.Request.Headers["Ticks"].ToString());
            var diffTime = DateTime.UtcNow - (new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(model.Ticks));
            var expirTime = TimeSpan.FromSeconds(300);// 过期工夫
            if (diffTime > expirTime)
            {await this.ResponseValidFailedAsync(context, 504);
                return;
            }

验证申请 ID

验证申请 ID 是不是有在申请头里传过来,没有就返回失败
验证申请 ID 是不是曾经在缓存里存在,如果存在就示意反复申请,那么就返回失败
如果申请 ID 在缓存中不存在,那么就示意失常的申请,同时把申请 ID 增加到缓存

            model.RequestId = context.Request.Headers["RequestId"];
            if (String.IsNullOrEmpty(model.RequestId))
            {await this.ResponseValidFailedAsync(context, 505);
                return;
            }
            var cacheKey = $"RequestValidSign:RequestId:{model.AppId}:{model.RequestId}";
            if (cacheSvc.TryGetValue(cacheKey, out _))
            {await this.ResponseValidFailedAsync(context, 506);
                return;
            }
            else
                cacheSvc.Set(cacheKey, model.RequestId, expirTime);

验证签名

1. 验证签名是否失常
2. 签名字符串是 $”{AppId}{Ticks}{RequestId}{AppSecret}” 组成
3. 而后把签名字符串做 MD5,再与申请传过来的 Sign 签名比照
4. 如果一至就示意失常申请,申请通过。如果不一至,返回失败


    public bool Valid()
    {var validStr = $"{AppId}{Ticks}{RequestId}{AppSecret}";
        return validStr.ToMD5String() == Sign;}

            model.Sign = context.Request.Headers["Sign"];
            if (!model.Valid())
            {await this.ResponseValidFailedAsync(context, 507);
                return;
            }

源代码

咱们把所有代码写成一个 Asp.Net Core 的中间件


/// <summary>
/// 申请签名验证
/// </summary>
public class RequestValidSignMiddleware
{
    private readonly RequestDelegate _next;

    public RequestValidSignMiddleware(RequestDelegate next)
    {_next = next;}

    public async Task InvokeAsync(HttpContext context)
    {var model = new RequestValidSignModel();
        //1. 先验证 AppId 是不是有, 没有就间接返回失败
        //2. 如果有的话, 就去缓存里取 AppID 对应的配置(如果缓存里没有, 就去配置文件里取)
        //3. 如果没有对应 AppId 的配置, 阐明不是正确的申请, 返回失败
        model.AppId = context.Request.Headers["AppId"];
        if (String.IsNullOrEmpty(model.AppId))
        {await this.ResponseValidFailedAsync(context, 501);
            return;
        }
        var cacheSvc = context.RequestServices.GetRequiredService<IMemoryCache>();
        var cacheAppIdKey = $"RequestValidSign:APPID:{model.AppId}";
        var curConfig = cacheSvc.GetOrCreate<AppConfigModel>(cacheAppIdKey, (e) =>
        {e.SlidingExpiration = TimeSpan.FromHours(1);
            var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
            var listAppConfig = configuration.GetSection(AppConfigModel.ConfigSectionKey).Get<AppConfigModel[]>();
            return listAppConfig.SingleOrDefault(x => x.AppId == model.AppId);
        });
        if (curConfig == null)
        {await this.ResponseValidFailedAsync(context, 502);
            return;
        }
        //1. 把缓存 / 配置外面的 APP 配置取出来, 拿到 AppSecret
        //2. 如果申请里附带了 AppSecret(调试用), 那么就只验证 AppSecret 是否正确
        //3. 传过来的 AppSecret 必须是 Base64 编码后的
        //4. 而后比对传过来的 AppSecret 是否与配置的 AppSecret 一至, 如果一至就通过, 不一至就返回失败

        //5. 如果申请里没有附带 AppSecret, 那么走其它验证逻辑.
        model.AppSecret = curConfig.AppSecret;
        var headerSecret = context.Request.Headers["AppSecret"].ToString();
        if (!String.IsNullOrEmpty(headerSecret))
        {var secretBuffer = new byte[1024];
            var secretIsBase64 = Convert.TryFromBase64String(headerSecret, new Span<byte>(secretBuffer), out var bytesWritten);
            if (secretIsBase64 && Encoding.UTF8.GetString(secretBuffer, 0, bytesWritten) == curConfig.AppSecret)
                await _next(context);
            else
            {await this.ResponseValidFailedAsync(context, 508);
                return;
            }
        }
        else
        {
            //1. 验证工夫戳是不是有在申请头里传过来,没有就返回失败
            //2. 验证工夫戳与以后工夫比拟,如果不在过期工夫 (5 分钟) 之内的申请,就返回失败
            // 工夫戳为 1970 年 1 月 1 日到当初工夫的毫秒数(UTC 工夫)var ticksString = context.Request.Headers["Ticks"].ToString();
            if (String.IsNullOrEmpty(ticksString))
            {await this.ResponseValidFailedAsync(context, 503);
                return;
            }
            model.Ticks = long.Parse(context.Request.Headers["Ticks"].ToString());
            var diffTime = DateTime.UtcNow - (new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(model.Ticks));
            var expirTime = TimeSpan.FromSeconds(300);// 过期工夫
            if (diffTime > expirTime)
            {await this.ResponseValidFailedAsync(context, 504);
                return;
            }
            //1. 验证申请 ID 是不是有在申请头里传过来,没有就返回失败
            //2. 验证申请 ID 是不是曾经在缓存里存在,如果存在就示意反复申请,那么就返回失败
            //3. 如果申请 ID 在缓存中不存在,那么就示意失常的申请,同时把申请 ID 增加到缓存
            model.RequestId = context.Request.Headers["RequestId"];
            if (String.IsNullOrEmpty(model.RequestId))
            {await this.ResponseValidFailedAsync(context, 505);
                return;
            }
            var cacheKey = $"RequestValidSign:RequestId:{model.AppId}:{model.RequestId}";
            if (cacheSvc.TryGetValue(cacheKey, out _))
            {await this.ResponseValidFailedAsync(context, 506);
                return;
            }
            else
                cacheSvc.Set(cacheKey, model.RequestId, expirTime);
            //1. 验证签名是否失常
            //2. 签名字符串是 $"{AppId}{Ticks}{RequestId}{AppSecret}" 组成
            //3. 而后把签名字符串做 MD5,再与申请传过来的 Sign 签名比照
            //4. 如果一至就示意失常申请,申请通过。如果不一至,返回失败
            model.Sign = context.Request.Headers["Sign"];
            if (!model.Valid())
            {await this.ResponseValidFailedAsync(context, 507);
                return;
            }
            await _next(context);
        }
    }
    /// <summary>
    /// 返回验证失败
    /// </summary>
    /// <param name="context"></param>
    /// <param name="status"></param>
    /// <returns></returns>
    public async Task ResponseValidFailedAsync(HttpContext context, int status)
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new ComResult() {Success = false, Status = status, Msg = "申请签名验证失败"}, Extention.DefaultJsonSerializerOptions, context.RequestAborted);
    }
}
public class AppConfigModel
{
    public const string ConfigSectionKey = "AppConfig";
    /// <summary>
    /// 利用 Id
    /// </summary>
    public string AppId {get; set;}
    /// <summary>
    /// 利用密钥
    /// </summary>
    public string AppSecret {get; set;}
}
public class RequestValidSignModel : AppConfigModel
{
    /// <summary>
    /// 前端工夫戳
    /// Date.now()
    /// 1970 年 1 月 1 日 00:00:00 (UTC) 到以后工夫的毫秒数
    /// </summary>
    public long Ticks {get; set;}
    /// <summary>
    /// 申请 ID
    /// </summary>
    public string RequestId {get; set;}
    /// <summary>
    /// 签名
    /// </summary>
    public string Sign {get; set;}
    public bool Valid()
    {var validStr = $"{AppId}{Ticks}{RequestId}{AppSecret}";
        return validStr.ToMD5String() == Sign;}
}
中间件注册扩大
写一个中间件的扩大, 这样咱们在 Program 里能够不便的应用 / 停用中间件

/// <summary>
/// 中间件注册扩大
/// </summary>
public static class RequestValidSignMiddlewareExtensions
{public static IApplicationBuilder UseRequestValidSign(this IApplicationBuilder builder)
    {return builder.UseMiddleware<RequestValidSignMiddleware>();
    }
}
退出移动版