1. 程式人生 > >SDK (JWT身份驗證) 資料驗證過濾器

SDK (JWT身份驗證) 資料驗證過濾器

客戶端 (注意:SDK是客戶端的東西,客戶端呼叫SDK,通過SDK來請求我們的服務端)

什麼是sdk,sdk其實就是做最傻瓜化的封裝,別人能夠很有好的呼叫你的方法

這裡我們做一個客戶端像服務端發起http請求的例子,在這個例子中我們封裝一個.net的sdK

第一步:建立一個名字叫APISKD的類庫

建立一個類:SDKResult.cs

namespace APISKD
{
    /// <summary>
    /// SDK返回類
    /// </summary>
    class SDKResult
    {
        /// <summary>
        /// 響應報文體(內容)
        /// </summary>
        public string Result { get; set; }
        /// <summary>
        /// 響應狀態碼
        /// </summary>
        public HttpStatusCode StatusCode { get; set; }
    }
}

建立一個類:SDKClient.cs

namespace APISKD
{
    /// <summary>
    /// 這個類主要供內部呼叫,所以是私有的
    /// </summary>
    class SDKClient
    {
        private string appKey;
        private string appSecret;
        private string serverRoot; //網站根目錄 例如:http://127.0.0.1:80/api/v1

        public SDKClient(string appKey, string appSecret, string serverRoot)
        {
            this.appKey = appKey;
            this.appSecret = appSecret;
            this.serverRoot = serverRoot;
        }
        /// <summary>
        /// 非同步Get請求
        /// </summary>
        /// <param name="url">要請求的地址:例如Home/Get</param>
        /// <param name="requestParamsDic">請求引數鍵值對</param>
        /// <returns></returns>
        public async Task<SDKResult> GetAsync(string url, IDictionary<string, object> requestParamsDic)
        {
            if (requestParamsDic == null)
            {
                throw new ArgumentNullException("querystring請求引數不能為null");
            }
            //對請求引數的key按照升序排序,然後再用等號將key和value連線起來:例如 name=lily
            var rpOrderItems = requestParamsDic.OrderBy(r => r.Key).Select(r => r.Key + "=" + r.Value);
            var requestParamsStr = string.Join("&", rpOrderItems); //用&號拼接請求引數;例如:age=26&name=lily

            //使用經過升序排序後的引數字串+appSecret 對它進行MD5加密得到簽名sign
            var sign = Md5Helper.CalcMD5(requestParamsStr + appSecret);

            using (HttpClient hc = new HttpClient())
            {
                hc.DefaultRequestHeaders.Add("AppKey", appKey);//將appKey新增到請求報文頭中(做安全驗證)
                hc.DefaultRequestHeaders.Add("Sign", sign);//將簽名新增到請求報文頭中(做安全驗證)

                //請求全路徑。例如:http://127.0.0.1:80/api/v1/Home/Get
                var requestUrl = Path.Combine(serverRoot, url);
                var resp = await hc.GetAsync(requestUrl + "?" + requestParamsStr);//發起非同步請求

                SDKResult skdResult = new SDKResult();

                skdResult.Result = await resp.Content.ReadAsStringAsync(); //請求內容
                skdResult.StatusCode = resp.StatusCode; //請求狀態碼
                return skdResult;
            }
        }

        /// <summary>
        /// 非同步Post請求
        /// </summary>
        /// <param name="url">要請求的地址:例如Home/Get</param>
        /// <param name="requestParamsDic">請求引數鍵值對</param>
        /// <returns></returns>
        public async Task<SDKResult> PostAsync(string url, Dictionary<string, string> requestParamsDic)
        {
            if (requestParamsDic == null)
            {
                throw new ArgumentNullException("querystring請求引數不能為null");
            }
            //對請求引數的key按照升序排序,然後再用等號將key和value連線起來:例如 name=lily
            var rpOrderItems = requestParamsDic.OrderBy(r => r.Key).Select(r => r.Key + "=" + r.Value);
            var requestParamsStr = string.Join("&", rpOrderItems); //用&號拼接請求引數;例如:age=26&name=lily

            //使用經過升序排序後的引數字串+appSecret 對它進行MD5加密得到簽名sign
            var sign = Md5Helper.CalcMD5(requestParamsStr + appSecret);


            using (HttpClient hc = new HttpClient())
            {
                FormUrlEncodedContent content = new FormUrlEncodedContent(requestParamsDic);

                //請求全路徑。例如:http://127.0.0.1:80/api/v1/Home/Get
                var requestUrl = Path.Combine(serverRoot, url);
                var resp = await hc.PostAsync(requestUrl, content);

                SDKResult skdResult = new SDKResult();

                skdResult.Result = await resp.Content.ReadAsStringAsync();
                skdResult.StatusCode = resp.StatusCode; //請求狀態碼
                return skdResult;
            }
        }
    }
}

現在我們就新增一個暴露給外面呼叫的類,例如 使用者操作類,我們暫且給它取名叫UserApi(只是命名中有個api,但是他不是一個api專案)

建立一個類:UserApi.cs (暴露在外,供客戶端呼叫)

namespace APISKD
{
    /// <summary>
    /// 我們在客戶端直接new這個類物件,傳入引數,呼叫方法就可以了
    /// </summary>
    public class UserApi
    {
        private string appKey;
        private string appSecret;
        private string serverRoot;
        public UserApi(string appKey, string appSecret, string serverRoot)
        {
            this.appKey = appKey;
            this.appSecret = appSecret;
            this.serverRoot = serverRoot;
        }

        public async Task<long> AddAsync(string phoneNum, string nickName, string password)
        {
            SDKClient client = new SDKClient(appKey, appSecret, serverRoot);
            Dictionary<string, string> data = new Dictionary<string, string>();
            data["phoneNum"] = phoneNum;
            data["nickName"] = nickName;
            data["password"] = password;
            var result = await client.PostAsync("User/AddNew", data);
            if (result.StatusCode == System.Net.HttpStatusCode.OK)
            {
                //因為返回的報文體是新增id:{5}
                //使用newtonsoft把json格式反序列化為long
                long id = JsonConvert.DeserializeObject<long>(result.Result); //需要安裝:Newtonsoft.Json
                return id;
            }
            else
            {
                throw new ApplicationException("新增失敗,狀態碼" + result.StatusCode + ",響應報文" + result.Result);
            }
        }
    }
}

建立一個控制檯應用程式:在裡面呼叫我們封裝的SDK類庫的UserApi類,實現新增資料使用者的目的

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            UserApi user = new UserApi("dfdfjpep","fdfjpejfefnmfdhfosh", "http://127.0.0.1:8888/api/v1/"); //初始化

            var result= user.AddAsync("18620998800", "tom", "123456").Result;//呼叫新增類(控制檯Main方法不能用async所以這裡用了Result)
        }
    }
}

服務端:WebApi

控制器

服務端中我們就簡單的放了一個控制器方法

namespace WebApi.Controllers
{
    public class UserController : ApiController
    {
        public IUsersRepository users { get; set; }

        [HttpPost]
        public string GetData()
        {
            
            return "";
        }
    }
}

資料防篡改驗證過濾器

並在webapi中我們還建立了一個身份驗證的過濾器

namespace WebApi
{
    public class MyAuthenticationAttribute : IAuthorizationFilter//也可以直接繼承AuthorizationFilterAttribute
    {
        public IAppInfosRepository app { get; set; }
        public bool AllowMultiple => true;

        public async Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            //獲得報文頭中的AppKey和Sign (我們與客戶端約定,在向服務端發起請求的時候,將AppKey和Sign放到請求報文頭中)
            IEnumerable<string> appKeys;
            if (!actionContext.Request.Headers.TryGetValues("AppKey", out appKeys)) //從請求報文頭中獲取AppKey
            {
                {
                    return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("報文頭中的AppKey為空") };
                }
            }
            IEnumerable<string> signs;
            if (!actionContext.Request.Headers.TryGetValues("Sign", out signs)) //從請求報文頭中獲取Sign
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("報文頭中的Sign為空") };
            }
            string appKey = appKeys.First();
            string sign = signs.First();
            var appInfo = await app.GetByAppKeyAsync(appKey);//從資料庫獲取appinfo這條資料(獲取AppKey,AppSecret資訊)
            if (appInfo == null)
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("不存在的AppKey") };
            }
            if (appInfo.IsEnable == "true")
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { Content = new StringContent("AppKey已經被封禁") };
            }

            string requestDataStr = ""; //請求引數字串
            List<KeyValuePair<string, string>> requestDataList = new List<KeyValuePair<string, string>>();//請求引數鍵值對
            if (actionContext.Request.Method == HttpMethod.Post) //如果是Post請求
            {
                //獲取Post請求資料  
                requestDataStr = GetRequestValues(actionContext);
                if (requestDataStr.Length > 0)
                {
                    string[] requestParamsKv = requestDataStr.Split('&');
                    foreach (var item in requestParamsKv)
                    {
                        string[] pkv = item.Split('=');
                        requestDataList.Add(new KeyValuePair<string, string>(pkv[0], pkv[1]));
                    }
                    //requestDataList就是按照key(引數的名字)進行排序的請求引數集合   
                    requestDataList = requestDataList.OrderBy(kv => kv.Key).ToList();
                    var segments = requestDataList.Select(kv => kv.Key + "=" + kv.Value);//拼接key=value的陣列
                    requestDataStr = string.Join("&", segments);//用&符號拼接起來
                }

            }
            if (actionContext.Request.Method == HttpMethod.Get) //如果是Get請求
            {
                //requestDataList就是按照key(引數的名字)進行排序的請求引數集合          
                requestDataList = actionContext.Request.GetQueryNameValuePairs().OrderBy(kv => kv.Key).ToList();
                var segments = requestDataList.Select(kv => kv.Key + "=" + kv.Value);//拼接key=value的陣列
                requestDataStr = string.Join("&", segments);//用&符號拼接起來
            }

            //計算Sign (即:計算requestDataStr+AppSecret的md5值)
            string computedSign = MD5Helper.ComputeMd5(requestDataStr + appInfo.AppSecret);

            //使用者傳進來md5值和計算出來的比對一下,就知道資料是否有被篡改過
            if (sign.Equals(computedSign, StringComparison.CurrentCultureIgnoreCase))
            {
                return await continuation();
            }
            else
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("sign驗證失敗") };
            }
        }


        /// <summary>
        /// 獲取Post請求的請求引數內容
        /// 參考資料:https://www.cnblogs.com/hnsongbiao/p/7039666.html
        /// </summary>
        /// <param name="actionContext"></param>
        /// <returns></returns>
        public string GetRequestValues(HttpActionContext actionContext)
        {
            Stream stream = actionContext.Request.Content.ReadAsStreamAsync().Result;
            Encoding encoding = Encoding.UTF8;
            /*
                這個StreamReader不能關閉,也不能dispose, 關了就傻逼了
                因為你關掉後,後面的管道  或攔截器就沒辦法讀取了
                所有這裡不要用using
                using (StreamReader reader = new StreamReader(stream, System.Text.Encoding.UTF8))
                {
                    result = reader.ReadToEnd().ToString();
                }
            */
            var reader = new StreamReader(stream, encoding);
            string result = reader.ReadToEnd();
            /*
            這裡也要注意:   stream.Position = 0;
            當你讀取完之後必須把stream的位置設為開始
            因為request和response讀取完以後Position到最後一個位置,交給下一個方法處理的時候就會讀不到內容了。
            */
            stream.Position = 0;
            return result;
        }
    }
}

這裡提供一個JWT輔助類

using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using UserCenter.NETSDK;

namespace IM.Web
{
    public class JWTHelper
    {
        private static readonly string jwt_secret;

        static JWTHelper()
        {
            jwt_secret = ConfigurationManager.AppSettings["JWT_Secret"];
        }

        /// <summary>
        /// 把user加密放到JWT字串中
        /// </summary>
        /// <param name="user"></param>
        /// <returns>JWT字串</returns>
        public static string Encrypt(User user)
        {
            IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
            IJsonSerializer serializer = new JsonNetSerializer();
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);

            var token = encoder.Encode(user, jwt_secret);
            return token;
        }

        /// <summary>
        /// 從JWT token中解密出來User
        /// </summary>
        /// <param name="token"></param>
        /// <returns>如果token錯誤、被篡改或者過期,則返回null</returns>
        public static User Decrypt(string token)
        {
            try
            {
                IJsonSerializer serializer = new JsonNetSerializer();
                IDateTimeProvider provider = new UtcDateTimeProvider();
                IJwtValidator validator = new JwtValidator(serializer, provider);
                IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
                IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);

                var user = decoder.DecodeToObject<User>(token, jwt_secret, verify: true);
                return user;
            }
            catch (TokenExpiredException)
            {
                return null;
            }
            catch (SignatureVerificationException)
            {
                return null;
            }
        }

        /// <summary>
        /// 獲得當前登入使用者的User(供asp.net mvc用)
        /// </summary>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        public static User GetUser(HttpContextBase httpContext)
        {
            var cookie = httpContext.Request.Cookies["JWTToken"];
            if(cookie==null)
            {
                return null;
            }
            string token = cookie.Value;
            return Decrypt(token);
        }

        /// <summary>
        /// 獲得當前登入使用者的User(供SignalR的Hub用)
        /// </summary>
        /// <param name="hubContext"></param>
        /// <returns></returns>
        public static User GetUser(HubCallerContext hubContext)
        {
            if(!hubContext.RequestCookies.ContainsKey("JWTToken"))
            {
                return null;
            }
            string token = hubContext.RequestCookies["JWTToken"].Value;
            return Decrypt(token);
        }
    }
}