共计 6856 个字符,预计需要花费 18 分钟才能阅读完成。
前言
当初的零碎后端开发的时候, 会公开很多 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 = $"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 = $"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>(); | |
} | |
} |