前言

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

客户端调用实现

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

最终效果

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

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

WebAPI公开接口请求签名验证的更多相关文章

  1. [.Net]Framwork WebAPI添加接口请求监控

    思路: 通过重写 ActionFilterAttribute 拦截Action的请求及返回信息,实现对接口请求的监听. 最终效果如下: 全局启用需配置如下: 局部启用需配置如下: 源码如下: 1 // ...

  2. WebApi接口请求失败,找不到资源。

    WebApi开发接口,实现同步数据库的数据给安卓. public class UserInfoController : ApiControllerBase { private UserBLL user ...

  3. ASP.NET WebApi服务接口如何防止重复请求实现HTTP幂等性

    一.背景描述与课程介绍 明人不说暗话,跟着阿笨一起玩WebApi.在我们平时开发项目中可能会出现下面这些情况; 1).由于用户误操作,多次点击网页表单提交按钮.由于网速等原因造成页面卡顿,用户重复刷新 ...

  4. ASP.NET Core 入门(2)(WebApi接口请求日志 Request和Response)

    以前 .NET Framework WebApi 记录接口访问日志,一般是通过Filter的方式进行拦截,通过重写ActionFilterAttribute的OnActionExecuting实现拦截 ...

  5. WebApi安全性 使用TOKEN+签名验证 (秘钥是GUID的,私有的,不是雙方的,并不在网络连接上传输)

    转http://www.cnblogs.com/MR-YY/archive/2016/10/18/5972380.html WebApi安全性 使用TOKEN+签名验证   首先问大家一个问题,你在写 ...

  6. Asp.Net WebAPI配置接口返回数据类型为Json格式

    Asp.Net WebAPI配置接口返回数据类型为Json格式   一.默认情况下WebApi 对于没有指定请求数据类型类型的请求,返回数据类型为Xml格式 例如:从浏览器直接输入地址,或者默认的XM ...

  7. spring boot:给接口增加签名验证(spring boot 2.3.1)

    一,为什么要给接口做签名验证? 1,app客户端在与服务端通信时,通常都是以接口的形式实现, 这种形式的安全方面有可能出现以下问题: 被非法访问(例如:发短信的接口通常会被利用来垃圾短信) 被重复访问 ...

  8. Postman - 功能强大的 API 接口请求调试和管理工具

    Postman 是一款功能强大的的 Chrome 应用,可以便捷的调试接口.前端开发人员在开发或者调试 Web 程序的时候是需要一些方法来跟踪网页请求的,用户可以使用一些网络的监视工具比如著名的 Fi ...

  9. xmlrpc实现bugzilla api调用(无会话保持功能,单一接口请求)

    xmlrpc实现bugzilla4   xmlrpc api调用(无会话保持功能,单一接口请求),如需会话保持,请参考我的另外一篇随笔(bugzilla4的xmlrpc接口api调用实现分享: xml ...

  10. Loadrunner模拟JSON接口请求进行测试

    Loadrunner模拟JSON接口请求进行测试     一.loadrunner脚本创建 1.Insert - New step -选择Custom Request -  web_custom_re ...

随机推荐

  1. Java学习笔记06

    1. 类和对象 1.1 类和对象 ​ 客观存在的事物皆为对象,所以我们也常常说万物皆对象. 类 类的理解 类是对现实生活中一类具有共同属性和行为的事物的抽象 类是对象的数据类型,类是具有相同属性和行为 ...

  2. 省市县树形结构打印-.netCore控制台程序

    using CityJson;using Dapper;using Newtonsoft.Json;{ using (var db = DbHelper.Db()) { //数据格式 //code_p ...

  3. Django框架——静态文件配置、form表单、request对象、连接数据库、ORM简介、ORM基本操作和语句

    配置文件介绍 SECRET_KEY = '0yge9t5m9&%=of**qk2m9z^7-gp2db)g!*5dzb136ys0#)*%*a' # 盐 DEBUG = True # 调试模式 ...

  4. 【kafka】-分区-消费端负载均衡

    一.为什么kafka要做分区? 因为当一台机器有可能扛不住(类比:就像redis集群中的redis-cluster一样,一个master抗不住写,那么就多个master去抗写),把一个队列的单一mas ...

  5. 2023-05-07:给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。 返回执行此操作后,grid 中最大的岛屿面积是多少? 岛屿 由一组上、下、左、右四个方向相

    2023-05-07:给你一个大小为 n x n 二进制矩阵 grid .最多 只能将一格 0 变成 1 . 返回执行此操作后,grid 中最大的岛屿面积是多少? 岛屿 由一组上.下.左.右四个方向相 ...

  6. 做了个vscode 小插件,用于修改window 的颜色以区分同时打开的不同工作区,快用起来吧!

    Coralize marketplace/coralize 以高效且便捷的方式自定义Visual Studio Code工作区窗口的状态栏.标题栏以及活动边栏等颜色!这将对那些需要同时打开多个vsco ...

  7. SqlParameter的作用与用法

    有时候为图方便,会直接用sqlhelper文件进行相关操作,会出现如下的类: public static object ExecuteScalar(string sqlStr, params SqlP ...

  8. 2023-03-03:请用go语言调用ffmpeg,摄像头捕获并编码为h264文件,不管音频。

    2023-03-03:请用go语言调用ffmpeg,摄像头捕获并编码为h264文件,不管音频. 答案2023-03-03: 使用 github.com/moonfdd/ffmpeg-go 库. 先用如 ...

  9. 2023-02-20:小A认为如果在数组中有一个数出现了至少k次, 且这个数是该数组的众数,即出现次数最多的数之一, 那么这个数组被该数所支配, 显然当k比较大的时候,有些数组不被任何数所支配。 现在

    2023-02-20:小A认为如果在数组中有一个数出现了至少k次, 且这个数是该数组的众数,即出现次数最多的数之一, 那么这个数组被该数所支配, 显然当k比较大的时候,有些数组不被任何数所支配. 现在 ...

  10. 2021-02-09:如何删除一个链表的倒数第n个元素?

    2021-02-09:如何删除一个链表的倒数第n个元素? 福哥答案2021-02-09: 1.创建虚拟头元素,虚拟头元素的Next指针指向头元素.2.根据快慢指针求倒数第n+1个元素,假设这个元素是s ...