介面鑑權之sign簽名校驗與JWT驗證

那一片藍海發表於2021-01-31

需求描述:

  專案裡的幾個Webapi介面需要進行鑑權,同介面可被小程式或網頁呼叫,小程式裡沒有使用者登入的概念,網頁裡有使用者登入的概念,對於呼叫方來源是小程式的情況下進行放權,其他情況下需要有身份驗證。也就是說給所有小程式請求進行放行,給網頁請求進行jwt身份驗證。由於我的小程式沒有使用者登入的功能,所以要針對小程式和網頁設計出兩套完全不同的鑑權方式。

  

鑑權流程設計:

  查閱相關資料,最終決定的鑑權方式:

  • 小程式採用sign簽名檢驗
  • 網頁採用目前比較流行的JWT的token校驗

通過AOP的思想使用.Net的Attribute進行攔截請求

 程式碼實現

  主要是服務端寫一個Attribute,判斷是小程式還是網頁,然後採用不同的兩種不同的鑑權方式。

  

  • Attribute程式碼:
    public class WxAllowFilterAttribute : BaseActionFilter
    {
        private static readonly int _errorCode = 401;
        public override void OnActionExecuting(HttpActionContext filterContext)
        {
            var iswx = filterContext.iswx();//判斷是否是小程式發來的請求
            if (iswx)
            {
          //小程式的簽名校驗
if (!filterContext.checkwx()) { filterContext.Response = Error("小程式簽名驗證失敗", _errorCode); }; } else {
          //JWT的token校驗
string token = filterContext.GetToken(); if (string.IsNullOrEmpty(token)) { filterContext.Response = Error("缺少token", _errorCode); return; } if (!JWTHelper.CheckToken(token, JWTHelper.JWTSecret)) { filterContext.Response = Error("token校驗失敗!", _errorCode); return; } var payload = JWTHelper.GetPayload<JWTPayload>(token); if (payload.Expire < DateTime.Now) { filterContext.Response = Error("token過期!", _errorCode); return; } base.OnActionExecuting(filterContext); } } }

 

  • 擴充套件類
public static class HttpRequest
    {
        public static readonly string wx_secret = ConfigurationManager.AppSettings["wx_secret"];
        /// <summary>
        /// 獲取Token
        /// </summary>
        /// <param name="req">請求</param>
        /// <returns></returns>
        public static string GetToken(this HttpActionContext req)
        {
            string tokenHeader = req.Request.Headers.Authorization == null ? "" : req.Request.Headers.Authorization.Parameter;
            if (string.IsNullOrEmpty(tokenHeader))
                return null;
            string pattern = "^Bearer (.*?)$";
            if (!Regex.IsMatch(tokenHeader, pattern))
                throw new Exception("token格式不對!格式為:Bearer {token}");

            string token = Regex.Match(tokenHeader, pattern).Groups[1].ToString();
            if (string.IsNullOrEmpty(token))
                throw new Exception("token不能為空!");
            return token;
        }
        /// <summary>
        /// 判斷是否微信
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        public static bool iswx(this HttpActionContext req)
        {
            var queryList = req.Request.RequestUri.Query.Split('&').ToList<string>();
            Dictionary<String, String> pList = new Dictionary<String, String>();
            if (queryList.Count < 2)
            {
                return false;
            }
            else
            {
                queryList.ForEach(x =>
                {
                    var a = x.Split('=');
                    if (a.Count() >= 2)
                    {
                        pList.Add(a[0], a[1]);
                    }
                });
                var iswx = pList.Any(x => x.Key == "app_key" && x.Value == "wx");//判斷是否有微信標識的欄位
                return iswx;
            }


        }
        /// <summary>
        /// 檢驗微信sign是否合法
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        public static bool checkwx(this HttpActionContext req)
        {
            var queryList = req.Request.RequestUri.Query.Split('&').ToList<string>();
            Dictionary<String, String> pList = new Dictionary<String, String>();
            queryList.ForEach(x =>
            {
                var a = x.Split('=');
                if (a.Count() >= 2)
                {
                    pList.Add(a[0], a[1]);
                }
            });
            var app_key = pList["app_key"];
            var app_secret = wx_secret;
            var timetamp = pList["timestamp"];
            var sign = pList["sign"];
            if (!string.IsNullOrEmpty(timetamp)) { 
                var tamp=Convert.ToInt64(timetamp);
                var nowtamp = ToTimestamp(DateTime.Now);
                var a = nowtamp-tamp;
                if (a >= 15) {
                    return false;
                }
            }
            StringBuilder sb = new StringBuilder();
            sb.Append(app_key);
            sb.Append(app_secret);
            sb.Append(timetamp);
            var newsign = GetMD5(sb.ToString());
            return newsign == sign;

        }
        public static string GetMD5(string sDataIn)
        {
            MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
            byte[] bytes = Encoding.UTF8.GetBytes(sDataIn);
            byte[] buffer2 = provider.ComputeHash(bytes);
            provider.Clear();
            string str = "";
            for (int i = 0; i < buffer2.Length; i++)
            {
                str = str + buffer2[i].ToString("X").PadLeft(2, '0');
            }
            return str.ToLower();
        }

        public static long ToTimestamp(this DateTime target)
        {
            return (target.ToUniversalTime().Ticks - 621355968000000000) / 10000000;
        }
    }

 

  • Filter基類
public class BaseActionFilter : ActionFilterAttribute
    {
        //public virtual void OnActionExecuting(HttpActionContext filterContext)
        //{
        //}
        //public virtual void OnActionExecuted(HttpActionContext filterContext)
        //{
        //}
        /// <summary>
        /// 返回JSON
        /// </summary>
        /// <param name="json">json字串</param>
        /// <returns></returns>
        public HttpResponseMessage JsonContent(string json)
        {
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            return new HttpResponseMessage { Content = content, StatusCode = HttpStatusCode.OK };
        }
        public HttpResponseMessage IsSuccess()
        {
            AjaxResult res = new AjaxResult
            {
                IsSuccess = true,
                Msg = "請求成功!"
            };

            return JsonContent(JsonHelper.SerializeObject(res));
        }

        /// <summary>
        /// 返回成功
        /// </summary>
        /// <param name="msg">訊息</param>
        /// <returns></returns>
        public HttpResponseMessage IsSuccess(string msg)
        {
            AjaxResult res = new AjaxResult
            {
                IsSuccess = true,
                Msg = msg
            };

            return JsonContent(JsonHelper.SerializeObject(res));
        }

        /// <summary>
        /// 返回成功
        /// </summary>
        /// <param name="data">返回的資料</param>
        /// <returns></returns>
        public HttpResponseMessage IsSuccess<T>(T data)
        {
            AjaxResult<T> res = new AjaxResult<T>
            {
                IsSuccess = true,
                Msg = "請求成功!",
                Data = data
            };

            return JsonContent(JsonHelper.SerializeObject(res));
        }

        /// <summary>
        /// 返回錯誤
        /// </summary>
        /// <returns></returns>
        public HttpResponseMessage Error()
        {
            AjaxResult res = new AjaxResult
            {
                IsSuccess = false,
                Msg = "請求失敗!"
            };

            return JsonContent(JsonHelper.SerializeObject(res));
        }

        /// <summary>
        /// 返回錯誤
        /// </summary>
        /// <param name="msg">錯誤提示</param>
        /// <returns></returns>
        public HttpResponseMessage Error(string msg)
        {
            AjaxResult res = new AjaxResult
            {
                IsSuccess = false,
                Msg = msg,
            };

            return JsonContent(JsonHelper.SerializeObject(res));
        }

        /// <summary>
        /// 返回錯誤
        /// </summary>
        /// <param name="msg">錯誤提示</param>
        /// <param name="errorCode">錯誤程式碼</param>
        /// <returns></returns>
        public HttpResponseMessage Error(string msg, int errorCode)
        {
            AjaxResult res = new AjaxResult
            {
                IsSuccess = false,
                Msg = msg,
                StatusCode = errorCode
            };

            return JsonContent(JsonHelper.SerializeObject(res));
        }
    }

 

  • JWT擴充套件類
public class JWTHelper
    {
        private static readonly string _headerBase64Url = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".Base64UrlEncode();
        public static readonly string JWTSecret = ConfigurationManager.AppSettings["JWTSecret"];
        /// <summary>
        /// 生成Token
        /// </summary>
        /// <param name="payloadJsonStr">資料JSON字串</param>
        /// <param name="secret">金鑰</param>
        /// <returns></returns>
        public static string GetToken(string payloadJsonStr, string secret)
        {
            string payloadBase64Url = payloadJsonStr.Base64UrlEncode();
            StringBuilder sb = new StringBuilder();
            StringBuilder sb1 = new StringBuilder();
            sb.AppendFormat("{0}", _headerBase64Url);
            sb.Append(".");
            sb.AppendFormat("{0}", payloadBase64Url);
            sb1 = sb;
            string sign = sb.ToString().ToHMACSHA256String(secret);

            string token = sb1.AppendFormat(".{0}", sign).ToString();

            return token;
        }


        /// <summary>
        /// 獲取Token中的資料
        /// </summary>
        /// <typeparam name="T">泛型</typeparam>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static T GetPayload<T>(string token)
        {
            if (string.IsNullOrEmpty(token))
            {
                return default(T);
            }
            return token.Split('.')[1].Base64UrlDecode().ToObject<T>();
        }

        /// <summary>
        /// 校驗Token
        /// </summary>
        /// <param name="token">token</param>
        /// <param name="secret">金鑰</param>
        /// <returns></returns>
        public static bool CheckToken(string token, string secret)
        {
            var items = token.Split('.');
            var oldSign = items[2];
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0}", items[0]);
            sb.AppendFormat(".{0}", items[1]);
            string newSign = sb.ToString().ToHMACSHA256String(secret);
            return oldSign == newSign;
        }
    }

檢驗使用者名稱密碼是否正確的業務介面程式碼這裡不貼了..

網頁客戶端的程式碼還沒寫完,主要思路就是判斷快取裡是否有token,沒有就去把使用者名稱密碼去呼叫服務端的登入介面拿到token後存到快取裡,之後的所有請求都在頭部帶上這個token。

小程式客戶端程式碼 :

在app.js中定義一個公共的promise請求方法,並帶上請求的引數(app_key,時間戳,md5加密後的sign等),這裡要注意區分get和post請求的區別,get是放在url後的,post是放在body裡的,要對傳參的格式要稍加處理

  request(params) {
    reqTime++;
    //載入彈框
    wx.showLoading({
      title: '載入中...',
      mask: true
    });
    //返回
    return new Promise((resolve, reject) => {
      var data = {
        app_key: this.globalData.app_key,
        timestamp: Math.round(new Date() / 1000),
        sign: ''
      }
      data.sign = utilMd5.hexMD5(`${this.globalData.app_key}${this.globalData.app_secret}${data.timestamp}`)
      if (params.method.toUpperCase() == 'POST') {
        if (!params.url.includes('?')) {
          params.url += '?'
        }
        var url = `&app_key=${this.globalData.app_key}&timestamp=${data.timestamp}&sign=${data.sign}`
        params = {
          ...params,
          url: params.url + url
        }
        data = params.data
      } else {
        data = {
          ...params.data,
          ...data
        }
      }
      params = {
        ...params,
        data: {
          ...data
        }
      }
      wx.request({
        //解構params獲取請求引數
        ...params,
        success: (result) => {
          resolve(result);
        },
        fail: (err) => {
          reject(err);
        },
        complete: () => {
          reqTime--;
          //停止載入
          if (!reqTime)
            wx.hideLoading();
        }
      });
    });
  }

 

這邊說明下,我的app_key和app_secret都是寫在app.js裡的公共變數中的,app_key在url裡是暴露的,但是app_secret是絕不能被暴露的。光知道app_key是無法生成正確的sign的,必須app_key,app_secret和timestap三者的加密才能生成正確的sign。我把app_secret寫在app.js中可能不是安全的做法,但是通過請求伺服器去獲取app.secret又要面臨網路請求的安全問題,最多對字串進行加密解密,但也不能說絕對安全了。app_secret怎麼處理最安全我目前也沒想到很好的辦法。。

好了,以上就是小程式的鑑權方法,小程式客戶端在請求時只需要呼叫這個公共方法就行。

鑑權測試結果

  • 給控制器或者方法前面加上鑑權的特性[WxAllowFilter] 

 

 

 

  • PostMan直接呼叫不帶任何sign等引數

  • 偽造小程式引數簽名驗證失敗或者時間戳超過10秒

  • 小程式內呼叫

 

相關文章