現在的系統後端開發的時候,會公開很多API介面
對於要登入認證後才能存取的介面,這樣的請求驗證就由身份認證模組完成
但是也有些介面是對外公開的,沒有身份認證的介面
我們怎麼保證介面的請求是合法的,有效的.
這樣我們一般就是對請求的合法性做簽名驗證.
為保證介面安全,每次請求必帶以下header
| header名 | 型別 | 描述 |
| AppId | string | 應用Id |
| Ticks | string | 時間戳為1970年1月1日到現在時間的毫秒數(UTC時間) |
| RequestId | string | GUID字串,作為請求唯一標誌,防止重複請求 |
| Sign| string | 簽名,簽名演演算法如下 |
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;
}
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);
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檔案
如果我們增加了請求驗證的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();
}
}
當我們沒有傳簽名引數的時候,返回失敗
當我們把簽名引數都傳正確後,返回正確