前言
现在的系统后端开发的时候,会公开很多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的配置,说明不是正确的请求,返回失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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; }
|
验证时间戳
- 验证时间戳是不是有在请求头里传过来,没有就返回失败
- 验证时间戳与当前时间比较,如果不在过期时间(5分钟)之内的请求,就返回失败
- 时间戳为1970年1月1日到现在时间的毫秒数(UTC时间)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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添加到缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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.如果一至就表示正常请求,请求通过。如果不一至,返回失败
1 2 3 4 5 6 7 8 9 10 11 12
| 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的中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
|
public class RequestValidSignMiddleware { private readonly RequestDelegate _next;
public RequestValidSignMiddleware(RequestDelegate next) { _next = next; }
public async Task InvokeAsync(HttpContext context) { var model = new RequestValidSignModel(); 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; }
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 { 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; } 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); 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"; public string AppId { get; set; } public string AppSecret { get; set; } } public class RequestValidSignModel : AppConfigModel { public long Ticks { get; set; } public string RequestId { get; set; } public string Sign { get; set; } public bool Valid() { var validStr = $"{AppId}{Ticks}{RequestId}{AppSecret}"; return validStr.ToMD5String() == Sign; } }
|
中间件注册扩展
写一个中间件的扩展,这样我们在Program里可以方便的使用/停用中间件
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public static class RequestValidSignMiddlewareExtensions { public static IApplicationBuilder UseRequestValidSign(this IApplicationBuilder builder) { return builder.UseMiddleware<RequestValidSignMiddleware>(); } }
app.UseRequestValidSign();
|
与Swagger结合
我们一般对外提供在线的Swagger文档
如果我们增加了请求验证的Header,那么所有接口文档里面都要把验证的Header添加到在线文档里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
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 = "请求签名", 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" } }); } }
builder.Services.AddSwaggerGen(c => { c.OperationFilter<RequestValidSignSwaggerOperationFilter>(); });
|

客户端调用实现
我们如果用HttpClient调用的话,就要在调用请求前
设置后请求头,AppId,Ticks,RequestId,Sign
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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(); } }
|
最终效果
当我们没有传签名参数的时候,返回失败

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