• WebAPI公开接口请求签名验证


    前言

    现在的系统后端开发的时候,会公开很多API接口
    对于要登录认证后才能访问的接口,这样的请求验证就由身份认证模块完成
    但是也有些接口是对外公开的,没有身份认证的接口
    我们怎么保证接口的请求是合法的,有效的.
    这样我们一般就是对请求的合法性做签名验证.

    实现原理

    为保证接口安全,每次请求必带以下header

    | header名 | 类型 | 描述 |
    | AppId | string | 应用Id |
    | Ticks | string | 时间戳为1970年1月1日到现在时间的毫秒数(UTC时间) |
    | RequestId | string | GUID字符串,作为请求唯一标志,防止重复请求 |
    | Sign| string | 签名,签名算法如下 |

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

    后端验证实现

    验证AppId

    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();
            var cacheAppIdKey = $"RequestValidSign:APPID:{model.AppId}";
            var curConfig = cacheSvc.GetOrCreate(cacheAppIdKey, (e) =>
            {
                e.SlidingExpiration = TimeSpan.FromHours(1);
                var configuration = context.RequestServices.GetRequiredService();
                var listAppConfig = configuration.GetSection(AppConfigModel.ConfigSectionKey).Get();
                return listAppConfig.SingleOrDefault(x => x.AppId == model.AppId);
            });
            if (curConfig == null)
            {
                await this.ResponseValidFailedAsync(context, 502);
                return;
            }
    

    验证时间戳

    1. 验证时间戳是不是有在请求头里传过来,没有就返回失败
    2. 验证时间戳与当前时间比较,如果不在过期时间(5分钟)之内的请求,就返回失败
    3. 时间戳为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

    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.如果一至就表示正常请求,请求通过。如果不一至,返回失败

        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的中间件

    /// 
    /// 请求签名验证
    /// 
    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();
            var cacheAppIdKey = $"RequestValidSign:APPID:{model.AppId}";
            var curConfig = cacheSvc.GetOrCreate(cacheAppIdKey, (e) =>
            {
                e.SlidingExpiration = TimeSpan.FromHours(1);
                var configuration = context.RequestServices.GetRequiredService();
                var listAppConfig = configuration.GetSection(AppConfigModel.ConfigSectionKey).Get();
                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(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);
            }
        }
        /// 
        /// 返回验证失败
        /// 
        /// 
        /// 
        /// 
        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";
        /// 
        /// 应用Id
        /// 
        public string AppId { get; set; }
        /// 
        /// 应用密钥
        /// 
        public string AppSecret { get; set; }
    }
    public class RequestValidSignModel : AppConfigModel
    {
        /// 
        /// 前端时间戳
        /// Date.now()
        /// 1970 年 1 月 1 日 00:00:00 (UTC) 到当前时间的毫秒数
        /// 
        public long Ticks { get; set; }
        /// 
        /// 请求ID
        /// 
        public string RequestId { get; set; }
        /// 
        /// 签名
        /// 
        public string Sign { get; set; }
        public bool Valid()
        {
            var validStr = $"{AppId}{Ticks}{RequestId}{AppSecret}";
            return validStr.ToMD5String() == Sign;
        }
    }
    

    中间件注册扩展

    写一个中间件的扩展,这样我们在Program里可以方便的使用/停用中间件

    /// 
    /// 中间件注册扩展
    /// 
    public static class RequestValidSignMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestValidSign(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware();
        }
    }
    
    ///Program.cs
    app.UseRequestValidSign();
    

    与Swagger结合

    我们一般对外提供在线的Swagger文档
    如果我们增加了请求验证的Header,那么所有接口文档里面都要把验证的Header添加到在线文档里面

    /// 
    /// 请求签名验证添加Swagger请求头
    /// 
    public class RequestValidSignSwaggerOperationFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            if (operation.Parameters == null)
                operation.Parameters = new List();
    
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = "AppId",
                In = ParameterLocation.Header,
                Required = true,
                Description = "应用ID",
                Schema = new OpenApiSchema
                {
                    Type = "string"
                }
            });
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = "Ticks",
                In = ParameterLocation.Header,
                Required = true,
                Description = "时间戳",
                Example = new OpenApiString(((long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds).ToString()),
                Schema = new OpenApiSchema
                {
                    Type = "string"
                }
            });
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = "RequestId",
                In = ParameterLocation.Header,
                Required = true,
                Description = "请求ID",
                Example = new OpenApiString(Guid.NewGuid().ToString()),
                Schema = new OpenApiSchema
                {
                    Type = "string"
                }
            });
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = "Sign",
                In = ParameterLocation.Header,
                Required = true,
                Description = "请求签名",
                //{AppId}{Ticks}{RequestId}{AppSecret}
                Example = new OpenApiString("MD5({AppId}{Ticks}{RequestId}{AppSecret})"),
                Schema = new OpenApiSchema
                {
                    Type = "string"
                }
            });
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = "AppSecret",
                In = ParameterLocation.Header,
                Description = "应用密钥(调试用)",
                Example = new OpenApiString("BASE64({AppSecret})"),
                Schema = new OpenApiSchema
                {
                    Type = "string"
                }
            });
        }
    }
    
    ///在Program.cs里添加Swagger请求验证Header
    builder.Services.AddSwaggerGen(c =>
    {
        c.OperationFilter();
    });
    

    客户端调用实现

    我们如果用HttpClient调用的话,就要在调用请求前
    设置后请求头,AppId,Ticks,RequestId,Sign

            public async Task<string> GetIPAsync(CancellationToken token)
            {
                this.SetSignHeader();
                var result = await Client.GetStringAsync("/Get", token);
                return result;
            }
            public void SetSignHeader()
            {
                this.Client.DefaultRequestHeaders.Clear();
                var ticks = ((long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds).ToString();
                var requestId = Guid.NewGuid().ToString();
                var signString = $"{this.Config.AppId}{ticks}{requestId}{this.Config.AppSecret}";
                var sign = this.GetMD5(signString);
                this.Client.DefaultRequestHeaders.Add("AppId", this.Config.AppId);
                this.Client.DefaultRequestHeaders.Add("Ticks", ticks);
                this.Client.DefaultRequestHeaders.Add("RequestId", requestId);
                this.Client.DefaultRequestHeaders.Add("Sign", sign);
            }
            public string GetMD5(string value)
            {
                using (MD5 md5 = MD5.Create())
                {
                    byte[] inputBytes = Encoding.UTF8.GetBytes(value);
                    byte[] hashBytes = md5.ComputeHash(inputBytes);
    
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < hashBytes.Length; i++)
                    {
                        sb.Append(hashBytes[i].ToString("x2"));
                    }
                    return sb.ToString();
                }
            }
    

    最终效果

    当我们没有传签名参数的时候,返回失败

    当我们把签名参数都传正确后,返回正确

  • 相关阅读:
    Python面向对象三大特征
    【Python笔记-设计模式】原型模式
    0 基础 Java 自学之路(4)
    “看看人家苹果和亚马逊”,嫌薪酬太低,谷歌员工“炮轰”高管
    百度搜索万亿规模特征计算系统实践
    Java 注解与反射
    接着聊聊如何从binlog文件恢复误delete的数据,模拟Oracle的闪回功能
    密码学与信息安全面试题及参考答案(2万字长文)
    后端开发就是增删改查?
    在线问诊配药的背后,看这家“数字化医院”如何守护数据流动安全
  • 原文地址:https://www.cnblogs.com/liuju150/p/WebApi-Request-Valid-Sign-Middleware.html