From 0e8ad966de5f02232a027d8f85de49304804710e Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Fri, 16 Jan 2026 10:11:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VOL.Core/Utilities/JSSaTokenSignHelper.cs | 124 ++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 VOL.Core/Utilities/JSSaTokenSignHelper.cs diff --git a/VOL.Core/Utilities/JSSaTokenSignHelper.cs b/VOL.Core/Utilities/JSSaTokenSignHelper.cs new file mode 100644 index 0000000..5a859ec --- /dev/null +++ b/VOL.Core/Utilities/JSSaTokenSignHelper.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace VOL.Core.Utilities +{ + public static class SaTokenSignHelper + { + /// + /// 根据业务参数生成 Java Sa-Token Sign 兼容的 QueryString + /// 自动生成 timestamp 和 nonce,如果外部未提供 + /// + /// 业务参数字典(可包含 teacherNo 等) + /// 应用密钥 + /// 应用编码,例如 sk-yuedong + /// 带 sign 的完整 QueryString + public static string BuildQueryString(Dictionary paramMap, string secretKey) + { + if (paramMap == null) + paramMap = new Dictionary(); + + // 1️⃣ TreeMap 排序(ASCII 升序) + var sorted = new SortedDictionary(StringComparer.Ordinal); + foreach (var kv in paramMap) + sorted[kv.Key] = kv.Value.ToString(); + + // 2️⃣ 拼接签名字符串 k=v&k=v&...&key=secretKey + var sbSign = new StringBuilder(); + foreach (var kv in sorted) + sbSign.Append(kv.Key).Append("=").Append(kv.Value).Append("&"); + sbSign.Append("key=").Append(secretKey.Trim()); + + string sign = Md5(sbSign.ToString()); + + // 3️⃣ 拼接 QueryString(保留外部字典原顺序) + var sbQuery = new StringBuilder(); + foreach (var kv in paramMap) + sbQuery.Append(Uri.EscapeDataString(kv.Key)) + .Append("=") + .Append(Uri.EscapeDataString(kv.Value.ToString())) + .Append("&"); + + sbQuery.Append("sign=").Append(sign); + + return sbQuery.ToString(); + } + + + /// + /// 构建 POST JSON 上传接口的 QueryString(包含 sign) + /// ⚠️ 不与 GET 共用,避免误用 + /// + public static string BuildPostJsonQueryString( + Dictionary paramMap, + string secretKey, + string jsonBody) + { + if (paramMap == null) + throw new ArgumentNullException(nameof(paramMap)); + + if (string.IsNullOrWhiteSpace(jsonBody)) + throw new ArgumentException("POST JSON body 不能为空"); + + // 1️⃣ 参与签名的参数(TreeMap + ASCII 排序) + var signParams = new SortedDictionary(StringComparer.Ordinal); + + foreach (var kv in paramMap) + signParams[kv.Key] = kv.Value.ToString(); + + // ⭐ POST JSON 特有:整个 body 作为 reqbody + signParams["reqbody"] = jsonBody; + + // 2️⃣ 拼接签名字符串 + // k=v&k=v&...&key=secretKey + var sbSign = new StringBuilder(); + foreach (var kv in signParams) + sbSign.Append(kv.Key) + .Append("=") + .Append(kv.Value) + .Append("&"); + + sbSign.Append("key=").Append(secretKey.Trim()); + + string sign = Md5(sbSign.ToString()); + + // 3️⃣ 构建 URL QueryString(⚠️ 不包含 reqbody) + var sbQuery = new StringBuilder(); + foreach (var kv in paramMap) + sbQuery.Append(Uri.EscapeDataString(kv.Key)) + .Append("=") + .Append(Uri.EscapeDataString(kv.Value.ToString())) + .Append("&"); + + sbQuery.Append("sign=").Append(sign); + + return sbQuery.ToString(); + } + + /// + /// MD5 小写 32 位 + /// + private static string Md5(string input) + { + using var md5 = MD5.Create(); + var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); + var sb = new StringBuilder(); + foreach (var b in bytes) + sb.Append(b.ToString("x2")); + return sb.ToString(); + } + + /// + /// 生成随机字符串 nonce,可指定长度 + /// + public static string RandomString(int length = 20) + { + const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + var random = new Random(); + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } + } +}