首页 > 其他分享 >WebAPI公开接口请求签名验证

WebAPI公开接口请求签名验证

时间:2024-07-17 16:11:32浏览次数:15  
标签:WebAPI 请求 接口 签名 context AppId var new model

前言

现在的系统后端开发的时候,会公开很多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<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. 验证时间戳是不是有在请求头里传过来,没有就返回失败

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

/// <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>();
	}
}

///Program.cs
app.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请求验证Header
builder.Services.AddSwaggerGen(c =>
{
	c.OperationFilter<RequestValidSignSwaggerOperationFilter>();
});

image

客户端调用实现

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

最终效果

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

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

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

文章转载至: https://www.cnblogs.com/liuju150/p/WebApi-Request-Valid-Sign-Middleware.html

标签:WebAPI,请求,接口,签名,context,AppId,var,new,model
From: https://www.cnblogs.com/laowu9877/p/18307613

相关文章

  • vue请求接口常用写法(axios)
    1.项目根目录下新建一个utils文件夹,并新建一个request.js文件(注意:是以axios方法请求的,所以需要先安装axios或cdn引入)安装:npmnpminstallaxios-Syarnyarnaddaxios-Scdn<scriptsrc="https://unpkg.com/axios/dist/axios.min.js"></script>&&配置代码imp......
  • iftop--实时网络接口流量监控工具
    1、介绍iftop是实时流量监控工具,可以用来监控网卡的实时流量(可以指定网段)、反向解析IP、显示端口信息等。 2、安装yum-yinstalliftop查看版本strings/usr/sbin/iftop|grep"version" 3、常用参数-h显示帮助信息-n不进行dns解析,直接显示ip地址-N显示端......
  • Java身份证识别接口在打车场景的应用
    曾几何时,滴滴打车似乎成了打车一族的首选,评论有好有坏。近几日,美团打车在上海的首秀还不错。为了吸引更多司机加入,美团还设置了新人推荐奖。根据美团打车司机端的信息显示,3月20和3月31日期间,司机每邀请一名新司机,并且新司机完成8单,即可拿到100元奖励。根据美团的宣传,其首日接......
  • 2024年最新版微信小程序批量检测封禁接口实现代码分享
    ​       ​      <?php//要检测的appid列表$appids=array('appid1','appid2','appid3');//使用实际的appid//循环调用接口检测小程序状态foreach($appidsas$appid){    $url='https://down.ychengsnsm.com/xcx/checkxcx.php?appid='......
  • python接口测试示例
    1、从sql读取需要的内容,作为接口的请求参数的一部分,生成请求的body,再去请求接口1importrequests2importpymysql3importjson456conn=pymysql.connect(host="172.16.11.118",port=3306,user="root",passwd="123456",db="digital_energy_base&......
  • uniapp [全端兼容] - 详细实现用户电子签名 “逐字校验“ 将姓名按字拆开分别手写签署
    前言如果您需要“合同专用”签字板及展示,请访问这篇文章。在uni-app全平台兼容(H5网页网站、支付宝/微信小程序、安卓App、苹果App、nvue)项目开发中,详解完成用户进行电子签名时,将其姓名进行拆分为独立的汉字,并由系统自动生成渲染对应的单个汉字文字的签名和验证笔画......
  • 未公开 泛微OA E-Cology 某接口SQL注入漏洞
    0x01阅读须知        技术文章仅供参考,此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等(包括但不限于)进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者......
  • 0day 新接口泛微e-cology getHendledWorkflowRequestList SQL注入漏洞
    0x01阅读须知        技术文章仅供参考,此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等(包括但不限于)进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者......
  • OpenSSL RSA相关基本接口和编程示例
    本文测试代码基于Openssl版本:1.1.1fRSA接口接口简介RSA对象创建intRSA_generate_key_ex(RSA*rsa,intbits,BIGNUM*e,BN_GENCB*cb);功能:创建⼀对rsa的公钥私钥参数:RSA密钥指针,密钥bit位数,公钥指数的⼤数形式指针,回调函数返回:成功返回1,失败返回0e主要有两个取值......
  • 接口幂等的几种方案
    接口幂等防重:防重设计主要为了避免产生重复数据接口幂等性:除了防止产生重复数据,还要求执行多次与执行一次产生的影响是一样的重复提交是造成的原因幂等性是要保证的结果针对操作查询、删除具有幂等性新增、修改不具有幂等性update操作1.如果只是单纯的更新数据,比如:upda......