前言

当初的零碎后端开发的时候,会公开很多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>();    }}