Procházet zdrojové kódy

feat: Signature 身份验证增加重放检测

许俊杰 před 2 roky
rodič
revize
c17b3ea2d6

+ 5 - 0
Admin.NET/Admin.NET.Core/Const/CacheConst.cs

@@ -76,4 +76,9 @@ public class CacheConst
     /// 开放接口身份缓存
     /// </summary>
     public const string KeyOpenAccess = "sys_open_access:";
+
+    /// <summary>
+    /// 开放接口身份随机数缓存
+    /// </summary>
+    public const string KeyOpenAccessNonce = "sys_open_access_nonce:";
 }

+ 5 - 0
Admin.NET/Admin.NET.Core/SignatureAuthentication/SignatureAuthenticationDefaults.cs

@@ -18,4 +18,9 @@ public static class SignatureAuthenticationDefaults
     /// SignatureAuthenticationOptions.AuthenticationScheme 使用的默认值
     /// </summary>
     public const string AuthenticationScheme = "Signature";
+
+    /// <summary>
+    /// 附加在在 HttpContext Item 中验证失败消息的 Key
+    /// </summary>
+    public const string AuthenticateFailMsgKey = "SignatureAuthenticateFailMsg";
 }

+ 21 - 10
Admin.NET/Admin.NET.Core/SignatureAuthentication/SignatureAuthenticationHandler.cs

@@ -19,12 +19,16 @@ namespace Admin.NET.Core;
 /// </summary>
 public sealed class SignatureAuthenticationHandler : AuthenticationHandler<SignatureAuthenticationOptions>
 {
+    private SysCacheService _cacheService;
+
     public SignatureAuthenticationHandler(IOptionsMonitor<SignatureAuthenticationOptions> options,
         ILoggerFactory logger,
         UrlEncoder encoder,
-        ISystemClock clock)
+        ISystemClock clock,
+        SysCacheService cacheService)
         : base(options, logger, encoder, clock)
     {
+        _cacheService = cacheService;
     }
 
     private new SignatureAuthenticationEvent Events
@@ -71,11 +75,17 @@ public sealed class SignatureAuthenticationHandler : AuthenticationHandler<Signa
 
         //校验签名
         var appSecretByte = Encoding.UTF8.GetBytes(accessSecret);
-        string serverSign = SignData(appSecretByte, GetMessageForSign(Request.Method, Request.Path, accessKey, timestamp, nonce));
+        string serverSign = SignData(appSecretByte, GetMessageForSign(Context));
 
         if (serverSign != sign)
             return await AuthenticateResultFailAsync("sign 无效的签名");
 
+        //重放检测
+        var cacheKey = $"{CacheConst.KeyOpenAccessNonce}{accessKey}|{nonce}";
+        if (_cacheService.ExistKey(cacheKey))
+            return await AuthenticateResultFailAsync("重复的请求");
+        _cacheService.Set(cacheKey, null, Options.AllowedDateDrift * 2);//缓存过期时间为偏差范围时间的2倍
+
         //已验证成功
         var signatureValidatedContext = new SignatureValidatedContext(Context, Scheme, Options)
         {
@@ -109,14 +119,15 @@ public sealed class SignatureAuthenticationHandler : AuthenticationHandler<Signa
     /// <summary>
     /// 获取用于签名的消息
     /// </summary>
-    /// <param name="method">请求方法(大写)</param>
-    /// <param name="url">请求 url,去除协议、域名、参数,以 / 开头</param>
-    /// <param name="accessKey">身份标识</param>
-    /// <param name="timestamp">时间戳,精确到秒</param>
-    /// <param name="nonce">6位随机数</param>
     /// <returns></returns>
-    private static string GetMessageForSign(string method, string url, string accessKey, long timestamp, string nonce)
+    private static string GetMessageForSign(HttpContext context)
     {
+        var method = context.Request.Method;//请求方法(大写)
+        var url = context.Request.Path;//请求 url,去除协议、域名、参数,以 / 开头
+        var accessKey = context.Request.Headers["accessKey"].FirstOrDefault();//身份标识
+        var timestamp = context.Request.Headers["timestamp"].FirstOrDefault();//时间戳,精确到秒
+        var nonce = context.Request.Headers["nonce"].FirstOrDefault();//唯一随机数
+
         return $"{method}&{url}&{accessKey}&{timestamp}&{nonce}";
     }
 
@@ -140,14 +151,14 @@ public sealed class SignatureAuthenticationHandler : AuthenticationHandler<Signa
     }
 
     /// <summary>
-    /// 返回验证失败结果,并在 Items 中增加 AuthenticateFailMsg,记录身份验证失败消息
+    /// 返回验证失败结果,并在 Items 中增加 <see cref="SignatureAuthenticationDefaults.AuthenticateFailMsgKey"/>,记录身份验证失败消息
     /// </summary>
     /// <param name="message"></param>
     /// <returns></returns>
     private Task<AuthenticateResult> AuthenticateResultFailAsync(string message)
     {
         //写入身份验证失败消息
-        Context.Items["AuthenticateFailMsg"] = message;
+        Context.Items[SignatureAuthenticationDefaults.AuthenticateFailMsgKey] = message;
         return Task.FromResult(AuthenticateResult.Fail(message));
     }
 }

+ 1 - 1
Admin.NET/Admin.NET.Core/Util/AdminResultProvider.cs

@@ -66,7 +66,7 @@ public class AdminResultProvider : IUnifyResultProvider
             case StatusCodes.Status401Unauthorized:
                 var msg = "401 登录已过期,请重新登录";
                 //20231005 如果存在身份验证失败消息,则返回消息内容
-                if (context.Items.TryGetValue("AuthenticateFailMsg", out var authFailMsg))
+                if (context.Items.TryGetValue(SignatureAuthenticationDefaults.AuthenticateFailMsgKey, out var authFailMsg))
                     msg = authFailMsg + "";
                 await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: msg),
                     App.GetOptions<JsonOptions>()?.JsonSerializerOptions);

+ 3 - 0
Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs

@@ -44,6 +44,9 @@ namespace Admin.NET.Web.Core
                 DefaultHttpContext currentHttpContext = context.GetCurrentHttpContext();
                 if (currentHttpContext == null)
                     return;
+                // 跳过由于 SignatureAuthentication 引发的失败
+                if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey))
+                    return;
                 currentHttpContext.SignoutToSwagger();
             }
         }