Java微服务-WebApi公开接口请求签名验证
前言
现在的系统后端开发的时候,会公开很多API接口
对于要登录认证后才能访问的接口,这样的请求验证就由身份认证模块完成
但是也有些接口是对外公开的,没有身份认证的接口
我们怎么保证接口的请求是合法的,有效的.
这样我们一般就是对请求的合法性做签名验证.
【资料图】
实现原理
为保证接口安全,每次请求必带以下header
| header名 | 类型 | 描述 |
| AppId | string | 应用Id |
| Ticks | string | 时间戳为1970年1月1日到现在时间的毫秒数(UTC时间) |
| RequestId | string | GUID字符串,作为请求唯一标志,防止重复请求 |
| Sign| string | 签名,签名算法如下 |
后端验证实现
验证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(); } }
最终效果
当我们没有传签名参数的时候,返回失败
当我们把签名参数都传正确后,返回正确
都看完了,你确定不点个赞,关注下再走?
关键词:
相关阅读
-
Java微服务-WebApi公开接口请求签名验证
|header名|类型|描述|。|Ticks|string|时间戳为1970年1月1日到现在时间的毫秒数|。 -
胡萝卜和它一起吃,脾胃更健康了,眼睛...
胡萝卜,被李时珍称为“菜蔬之王”,营养价值在众多蔬菜中可谓是出... -
星汉灿烂小说人物介绍(星汉灿烂里文家...
星汉灿烂中的文家是皇室家族,文帝,文修君,乾安王都是文氏家族的一员 -
星汉灿烂葛氏是什么身份(星汉灿烂葛氏...
电视剧《星汉灿烂》中的葛氏是由演员陈思斯饰演,陈思斯出道多年,她最 -
电梯行业端午节发客户短信祝福语_感恩节...
1、365天天快乐,4380月月顺心,8760时时有成。2、525600分分幸福,315 -
旧款HTCVIVE消费者版可抵扣1888元:现支...
千易网7月3日消息,HTCVIVE虚拟现实公众号发布消息,从今年7月1日-9月3 -
手动变速器故障修理多少钱(手动变速器...
1、手动变速器主要故障的现象有:⑴变速器漏油故障现象:变速器盖、侧 -
【环球新视野】击败ChatGPT?OpenChat霸...
【新智元导读】最能打的开源模型来了?OpenLLM在最新评测中,一举击败Ch -
红米8A打王者怎么样
红米8a打王者卡吗不会卡,而且其实也是比较好用的,其实他这个整体也算是 -
2023商业购物中心夏季西瓜主题活动美陈...
本方案分享的是《2023商业购物中心夏季西瓜主题活动美陈包装策划方案【 -
提醒!2024年11月CFA考试报名时间安排已出
CFA考试的2024年11月报名将于2024年2月8日开始!早鸟报名截止日期为202 -
环球热推荐:银行业动荡深刻影响美国经济
近期银行业的动荡揭示了目前部分美国银行所面临的困境,加剧了金融 -
永州市第四人民医院:危机时刻“救”在...
红网时刻新闻7月3日讯(通讯员郑建勇杨林华孙小倩)6月28日,永州市第 -
天天热议:内娱演员烂着烂着,就变成了“...
在看配音后的《甄嬛传》时,很多观众觉得一众主演的演技都挺好,但网上 -
长期资产适合率分析(长期资产)
期资产适合率分析,长期资产这个问题很多朋友还不知道,来为大家解答以 -
万达商管上半年总租金收入约263亿元 新...
7月3日,据万达商业管理官方微信号显示,万达商管披露了2023年上半年的 -
西宁旅游攻略二日游_西宁旅游攻略-天天快播
你们好,最近小未来发现有诸多的小伙伴们对于西宁旅游攻略二日游,西宁 -
【世界热闻】折纸花瓶教程_饮料瓶做花瓶...
1、准备一个玻璃直废饮料瓶。2、准备一把剪刀。3、用剪刀清洁饮料瓶的 -
环球最新:新款阿尔法罗密欧超级跑车已确认
首席设计师确认以1967年经典阿尔法33Stradale为灵感的全新旗舰跑车正在 -
人民日报一周好图+编辑点评2023/17_焦点热议
“人民日报一周好图+编辑点评”来啦!赏好图,看门道,带您外行秒变...