当前位置:科学 > 正文

Java微服务-WebApi公开接口请求签名验证|环球焦点

2023-07-03 14:08:12  来源:架构浅水湾

前言

现在的系统后端开发的时候,会公开很多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 = #34;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 = #34;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.签名字符串是#34;{AppId}{Ticks}{RequestId}{AppSecret}"组成
3.然后把签名字符串做MD5,再与请求传过来的Sign签名对比
4.如果一至就表示正常请求,请求通过。如果不一至,返回失败

public bool Valid()    {        var validStr = #34;{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 = #34;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 = #34;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.签名字符串是#34;{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 = #34;{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>();    }}///Program.csapp.UseRequestValidSign();

与Swagger结合

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

/// <summary>/// 请求签名验证添加Swagger请求头/// </summary>public class RequestValidSignSwaggerOperationFilter : IOperationFilter{    public void Apply(OpenApiOperation operation, OperationFilterContext context)    {        if (operation.Parameters == null)            operation.Parameters = new List<OpenApiParameter>();        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请求验证Headerbuilder.Services.AddSwaggerGen(c =>{    c.OperationFilter<RequestValidSignSwaggerOperationFilter>();});

客户端调用实现

我们如果用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 = #34;{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();            }        }

最终效果

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

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

都看完了,你确定不点个赞,关注下再走?

关键词:

推荐阅读

月壤形成的主要原因 月壤与土壤有什么区别

月壤形成的主要原因月壤形成过程没有生物活动参与,没有有机质,还极度缺水干燥;组成月壤的矿物粉末基本是由陨石撞击破砰形成,因此,粉末 【详细】

域名抢注是是什么意思?投资角度来看什么域名好?

域名抢注是是什么意思域名抢注是通过抢先注册的方式获得互联网删除的域名的使用权。域名是由点分隔的一串数字,用于标记一台计算机或一组计 【详细】

捷达保养费用是多少?捷达是哪个国家的品牌?

捷达保养费用是多少?全新捷达的保修期为2年或6万公里,以先到者为准,新车可享受一次免费保养,首次免费保养在5000-7500km或1年内进行。如 【详细】

天然气泄露会造成爆炸吗?天然气泄漏怎么办?

天然气泄露会造成爆炸吗?家里用的天然气如果泄露是会发生爆炸的。当空气中含有混合天然气时,在与火源接触的一系列爆炸危险中,就会发生爆 【详细】

四部门明确App收集个人信息范围 个人信息保护范围判断标准

四部门明确App收集个人信息范围近日,国家互联网信息办公室、工业和信息化部、公安部、国家市场监督管理总局联合印发《常见类型移动互联网 【详细】

相关新闻

关于我们  |  联系方式  |  免责条款  |  招聘信息  |  广告服务  |  帮助中心

联系我们:85 572 98@qq.com备案号:粤ICP备18023326号-40

科技资讯网 版权所有