.Net Web API 参数验签
前言
在开放的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(); } }