前言
在開放的api介面中,我們透過http Post或者Get請求伺服器的時候,會面臨著許多的安全性問題。為了保證資料在通訊時的安全性,我們可以採用TOKEN+引數簽名的方式來進行相關驗證。
- Token(本文使用jwt)來保證訪問介面的使用者身份合法。這種場景主要用於前端(網頁、移動端)的介面訪問身份驗證方式。前面一篇已經做了介紹:.net core web api授權、鑑權、API保護 - chenxizhaolu - 部落格園
- 用Sign引數簽名的方式來防止被篡改和請求的唯一性,是本篇介紹的重點。這種場景主要使用者給第三方提供API介面的身份驗證。
本文所有的實現程式碼可以參考:https://gitee.com/xiaoqingyao/web-app-identity.git
工作流程說明
1、作為服務的提供方,會在達成合作後為第三方提供一個AppId和AppSecret。
2、第三方透過這AppId和AppSecret請求某web介面拿到動態token。
3、第三方在請求其他業務介面時用AppId、時間戳、業務引數key和value按照一定規則拼裝字串、隨機數、token 拼接字串後計算md5值作為signKey,signKey跟appid、時間戳、隨機數一起放到Header中。(注:Token不傳)
3、伺服器在收到請求時,按照客戶端相同的規則計算SignKey,與客戶端Header傳來的值做比對,如果一致則認為驗籤透過。
接下來每一步驟進行詳細說明並提供關鍵程式碼。
1、AppId和AppSecret的維護
這個就很簡單了,就維護一個資料庫表進行維護AppId和AppSecret。新增一個合作伙伴的時候新增一條記錄即可。
2、獲取動態Token
/// <summary> /// 透過appkey與appSecret獲取token /// </summary> /// <param name="thirdClient"></param> /// <returns></returns> [HttpPost("GetToken")] [AllowAnonymous] public async Task<string> GetToken(ThirdClient thirdClient) { return await _thirdClientService.GetToken(thirdClient); }
public async Task<string> GetToken(ThirdClient thirdClient) { var cacheKey = "thirdClient_" + thirdClient.AppKey; if (_memoryCache.TryGetValue(cacheKey, out var cacheToken)) { return cacheToken?.ToString() ?? ""; } var exist = await _appDbContext.ThirdClients.AnyAsync(p => p.AppKey == thirdClient.AppKey && p.AppSecret == thirdClient.AppSecret); if (!exist) { return string.Empty; } var token = Guid.NewGuid().ToString().Replace("-", "").ToUpper(); _memoryCache.Set(cacheKey, token, TimeSpan.FromMinutes(10)); return token; }
3、客戶端請求及新增引數簽名、
internal class Program { static async Task Main(string[] args) { var token = await GetToken(); var getResult = await Get(token); Console.WriteLine("get請求結果:" + getResult); Thread.Sleep(1000); var postResult = await Post(token); Console.WriteLine("post請求結果:" + postResult); Thread.Sleep(1000); getResult = await Get(token); Console.WriteLine("get請求結果:" + getResult); Thread.Sleep(1000); postResult = await Post(token); Console.WriteLine("post請求結果:" + postResult); Console.WriteLine("Hello, World!"); } static async Task<string> Post(string token) { HttpClient client = new HttpClient(); var nonce = new Random().Next().ToString(); var appKey = "etcp"; var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // Unix 時間戳(秒) client.DefaultRequestHeaders.Add("AppKey", appKey); client.DefaultRequestHeaders.Add("TimeStamp", timeStamp.ToString()); client.DefaultRequestHeaders.Add("Nonce", nonce); IDictionary<string, string> sortedParams = new SortedDictionary<string, string> { { "id", "1" }, { "name", "張三" } }; IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); StringBuilder query = new StringBuilder(); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); } } string requestDataStr = query.ToString(); var sign = Md5Helper.ComputeMd5Hash(timeStamp.ToString() + nonce.ToString() + appKey.ToString() + token.ToString() + requestDataStr); client.DefaultRequestHeaders.Add("Sign", sign); var jsonContent = JsonSerializer.Serialize(sortedParams); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await client.PostAsync($"http://localhost:5203/SignProtectedAPI/PostApi", content); // 確保 HTTP 響應狀態是成功的 response.EnsureSuccessStatusCode(); // 讀取響應內容 var responseBody = await response.Content.ReadAsStringAsync(); return responseBody; } static async Task<string> Get(string token) { HttpClient client = new HttpClient(); var nonce = 5; var appKey = "etcp"; var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // Unix 時間戳(秒) client.DefaultRequestHeaders.Add("AppKey", appKey); client.DefaultRequestHeaders.Add("TimeStamp", timeStamp.ToString()); client.DefaultRequestHeaders.Add("Nonce", nonce.ToString()); IDictionary<string, string> sortedParams = new SortedDictionary<string, string> { { "id", "1" }, { "name", "張三" } }; IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); StringBuilder query = new StringBuilder(); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); } } string requestDataStr = query.ToString(); var sign = Md5Helper.ComputeMd5Hash(timeStamp.ToString() + nonce.ToString() + appKey.ToString() + token.ToString() + requestDataStr); client.DefaultRequestHeaders.Add("Sign", sign); var response = await client.GetAsync($"http://localhost:5203/SignProtectedAPI/List?{BuildQueryString(sortedParams.ToDictionary())}"); // 確保 HTTP 響應狀態是成功的 response.EnsureSuccessStatusCode(); // 讀取響應內容 var responseBody = await response.Content.ReadAsStringAsync(); return responseBody; } private static string BuildQueryString(Dictionary<string, string> parameters) { return string.Join("&", parameters.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}")); } static async Task<string> GetToken() { HttpClient client = new HttpClient(); //獲取token var jsonContent = "{\"appKey\":\"etcp\", \"appSecret\":\"34853399624948488e1b3cb63ea5e578\"}"; // key 與 secret提前提供 var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await client.PostAsync("http://localhost:5203/ThirdClient/GetToken", content); // 確保 HTTP 響應狀態是成功的 response.EnsureSuccessStatusCode(); var token = await response.Content.ReadAsStringAsync(); return token; } }
4、伺服器端驗證
新增並註冊一箇中介軟體SignValidateMiddleware。
public class SignValidateMiddleware { private readonly RequestDelegate _next; private readonly IServiceScopeFactory _scopeFactory; private readonly IMemoryCache _memoryCache; public SignValidateMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory, IMemoryCache memoryCache1) { _next = next; _scopeFactory = scopeFactory; _memoryCache = memoryCache1; } public async Task InvokeAsync(HttpContext context,IServiceProvider serviceProvider) { var requestPath = context.Request.Path; var exceptUrls = new string[] { "/User/", "/WeatherForecast/", "/swagger/", "/ThirdClient/" }; if (requestPath.HasValue && exceptUrls.Any(p => requestPath.Value.IndexOf(p) > -1)) { await _next(context); return; } if (!context.Request.Headers.TryGetValue("AppKey", out var appKey)) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤不透過-appKey"); return; } if (!context.Request.Headers.TryGetValue("TimeStamp", out var timeStamp) || string.IsNullOrEmpty(timeStamp)) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤不透過-TimeStamp"); return; } if (!context.Request.Headers.TryGetValue("Nonce", out var nonce)) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤不透過-Nonce"); return; } if (!context.Request.Headers.TryGetValue("Sign", out var sign)) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤不透過-Sign"); return; } bool timespanvalidate = double.TryParse(timeStamp.ToString(), out double ts1); double ts2 = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); double ts = ts2 - ts1; bool flag = ts > 1000 * 30;//30秒內有效 if (flag || !timespanvalidate) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤不透過-timespanvalidate"); return; } var cacheKey = "thirdClient_" + appKey; if (!_memoryCache.TryGetValue(cacheKey, out var token)) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("token驗證不透過,可能已經過期,請重新獲取token再試-token"); return; } var _userContext = serviceProvider.GetRequiredService<UserDbContext>() ?? throw new ArgumentNullException("未獲取到_userContext"); var thirdClient = _userContext.ThirdClients.FirstOrDefault(p => p.AppKey == appKey.ToString()); if (thirdClient == null) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤不透過-AppKey"); return; } IDictionary<string, string> parameters; if (context.Request.Method.Equals(HttpMethod.Get.ToString(), StringComparison.CurrentCultureIgnoreCase)) { parameters = new Dictionary<string, string>(); foreach (var kvp in context.Request.Query) { parameters.Add(kvp.Key, kvp.Value.ToString()); } } else if (context.Request.Method.Equals(HttpMethod.Post.ToString(), StringComparison.CurrentCultureIgnoreCase)) { var requestData = await new StreamReader(context.Request.Body).ReadToEndAsync(); parameters = JsonConvert.DeserializeObject<Dictionary<string, string>>(requestData ?? "{}") ?? []; // 將修改後的請求體重新寫入HttpContext,防止action拿不到資料包錯 if (!string.IsNullOrEmpty(requestData)) { byte[] requestBodyBytes = Encoding.UTF8.GetBytes(requestData); context.Request.Body = new MemoryStream(requestBodyBytes); } } else { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("不能識別的請求方法"); return; } string requestDataStr = GetSortedRequestDataStr(parameters); //請求引數字串拼接 string computedSign = Md5Helper.ComputeMd5Hash(timeStamp.ToString() + nonce.ToString() + appKey.ToString() + token.ToString() + requestDataStr); if (!sign.ToString().Equals(computedSign, StringComparison.CurrentCultureIgnoreCase)) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await context.Response.WriteAsync("驗籤驗證失敗"); return; } await _next(context); } public string GetSortedRequestDataStr(IDictionary<string, string> parameters) { IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters); IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); StringBuilder query = new StringBuilder(); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); } } return query.ToString(); } }
參考:ASP.NET WebApi TOKEN+簽名認證_tokensign.getvalue()-CSDN部落格