Api介面簽名驗證

Hcj發表於2020-06-16

通過特性來統一驗證的入口,實現ActionFilterAttribute介面來進行介面的簽名驗證

    /// <summary>
    /// 標準介面基類Controller
    /// </summary>
    [SignVerification]
    public abstract class BaseApiController : Controller
    {
    }
    
    /// <summary>
    /// 介面簽名驗證
    /// </summary>
    public class SignVerificationAttribute : ActionFilterAttribute
    {
    }

實現的思路為:

1.不同對接方的介面(外掛)定義不同的驗證key,不同的外掛間不能混用驗證key

2.不同的外掛生成不同的partnerId,partnerKey。請求的Url中需要攜帶partnerId,通過partnerId作為key在redis中找到對應的外掛驗證資訊(包括:partnerId,partnerKey等)

3.Url引數中必須包含partnerId,ts(時間戳),sign(加密簽名)。ts時間戳的有效時間為5分鐘,sign為(時間戳:formBody:partnerId:partnerKey)的MD5加密

4.如果通過partnerId可以找到對應的驗證資訊,再把(時間戳:formBody:partnerId:partnerKey)MD5加密後和sign比較確保請求沒有被篡改

5.確保partnerId為當前外掛而非其他外掛的,因為redis是共用的,只是通過key去取值而已

簽名方式

將時間戳和請求Form引數以及PartnerKey以冒號連線,如(時間戳:body:partnerId:PartnerKey)
將連線好的字串進行MD5生成sign

Url引數

引數說明型別必須備註
pid partnerId string  
ts 時間戳(格式:yyyyMMddHHmmss) string 時間戳的有效時間為5分鐘
sign MD5(時間戳:body:partnerId:pkey) string 參考簽名方式

具體程式碼實現

    /// <summary>
    /// 介面簽名驗證
    /// </summary>
    public class SignVerificationAttribute : ActionFilterAttribute
    {
        private readonly IDefaultUserService _defaultUserService;
        private readonly IInterfaceSignProvider _interfaceSignProvider;
        public SignVerificationAttribute()
        {
            _defaultUserService = ObjectContainer.GetService<IDefaultUserService>();
            _interfaceSignProvider = ObjectContainer.GetService<IInterfaceSignProvider>();
        }

        public void OnAuthentication(AuthenticationContext filterContext)
        {
            var request = filterContext.HttpContext.Request;
            var partnerId = request.QueryString["pid"];
            var timeStamp = request.QueryString["ts"];
            var sign = request.QueryString["sign"];//獲取Url引數
            var body = GetBodyText(request.InputStream);

            if (!ValidSign(filterContext,timeStamp, sign, body,partnerId,out IInterfaceSignInfo signInfo))//加密驗證
            {
                filterContext.Result = new ApiResult {Success = false, ErrorMessage = "無效簽名"};
                return;
            }

            var service = ObjectContainer.GetService<IAuthenticationService>();
            var userId = _defaultUserService.GetDefaultUserId(signInfo.LicNo);
            var identity = service.SignIn(userId, signInfo.LicNo, false, TimeSpan.FromMinutes(5), SessionType.WebApi);
            var newPrincipal = new GenericPrincipal(identity, new string[] { });
            filterContext.Principal = newPrincipal;
        }

        private static string GetBodyText(Stream stream)
        {
            using (var ms = new MemoryStream())
            {
                stream.CopyTo(ms);
                return Encoding.UTF8.GetString(ms.ToArray());
            }
        }

        private bool ValidSign(AuthenticationContext filterContext,string timeStamp, string sign, string body,string partnerId,out IInterfaceSignInfo signInfo)
        {
            signInfo = null;
            if (!string.IsNullOrEmpty(timeStamp) && !string.IsNullOrEmpty(sign)&& !string.IsNullOrEmpty(partnerId))
            {
                var cache = _interfaceSignProvider.GetInterfaceSignInfo(partnerId);//通過partnerId當key讀取redis
                if (cache.Enabled)
                {
                    var areaName = filterContext.RouteData.DataTokens["area"]?.ToString().ToLower();//獲取請求的area,即請求的是哪個外掛
                    if (string.IsNullOrEmpty(areaName) || !cache.PluginCode.ToLower().StartsWith(areaName))
                    {
                        return false;//PluginCode需以areaName開頭,否則意味著不是同一個外掛(如:PluginCode=juwov1,areaName=JuWo)
                    }
                    if (DateTime.TryParseExact(timeStamp, "yyyyMMddHHmmss", CultureInfo.CurrentCulture.DateTimeFormat, DateTimeStyles.AllowWhiteSpaces, out var time) &&
                        (DateTime.Now - time).TotalMinutes <= 5)//時間戳有效期為5分鐘
                    {
                        signInfo = cache;
                        var hashKey = EncryptHelper.Hash($"{timeStamp}:{body}:{partnerId}:{cache.PartnerKey}", "MD5").ToLowerInvariant();//MD5加密對比
                        return string.Equals(hashKey, sign);
                    }
                }
                
            }
            return false;
        }

    }

 

這樣就實現了介面的簽名驗證了。但是還有一個問題是,如果同時存在多個不同的對接介面(外掛)時,partnerId,PartnerKey應該是不一樣的。即外掛1和外掛2的驗證key是不能混用的。

可以通過路由來區分不同的外掛,來選擇進入不同的area,通過area來區分不同的外掛驗證key。

    public class JuWoAreaRegistration: AreaRegistration
    {
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "JuWo_default",
                "api/JuWo/{controller}/{action}/{id}",
                new {action = "Index", id = UrlParameter.Optional},
                new[] {"iERP.Its.Web.Areas.JuWo.Controllers"}
            );
        }

        public override string AreaName => "JuWo";
    }

 在之前的ValidSign方法中,通過var areaName = filterContext.RouteData.DataTokens["area"]?.ToString().ToLower();來獲取到當前請求的是哪個外掛,在把url上獲取到的partnerId與我們之前約定好的比較看是否能對應。

 

相關文章