前言

现在的系统后端开发的时候,会公开很多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>();
    }
}

时尚的黄花菜
1 声望0 粉丝