zoukankan      html  css  js  c++  java
  • 接口鉴权之sign签名校验与JWT验证

    需求描述:

      项目里的几个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秒

    • 小程序内调用

    转载请注明出处 个人网站:www.chenlinshan.com
  • 相关阅读:
    开发中常用的JS知识点集锦
    浏览器音频兼容和ffmpeg的音频转码使用
    web页面和小程序页面实现瀑布流效果
    微信小程序之支付密码输入demo
    Mac安装nginx配置过程
    前端工具mock的使用
    汇编语言学习
    Swift学习笔记
    如何快速融入团队并成为团队核心(四)
    如何快速融入团队并成为团队核心(三)
  • 原文地址:https://www.cnblogs.com/clsl/p/14353121.html
Copyright © 2011-2022 走看看