SDK (JWT身份驗證) 資料驗證過濾器
阿新 • • 發佈:2019-02-14
客戶端 (注意: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);
}
}
}