最近一段時間在學習WebApi,涉及到驗證部分的一些知識覺得自己並不是太懂,所以來博客園看了幾篇博文,發現一篇講的特別好的,讀了幾遍茅塞頓開(都閃開,我要裝逼了),剛開始讀有些地方不理解,所以想了很久,因此對原文中省略的部分這裡做一點個人的理解和補充,非常基礎,知道的園友就不需要了,只是幫助初次學習的園友理解。原文傳送門:
http://www.cnblogs.com/MR-YY/p/5972380.html#!comments
本篇博文中的所有代碼均來自上述鏈接,如果你覺得有幫助,請點擊鏈接給原文大牛一個推薦,開搞!!
1.基於Token令牌 + 簽名的驗證思路梳理
客戶端首先向服務端請求Token令牌,客戶獲取Token後計算對應的簽名。簽名由時間戳、隨機數、Token令牌、參數拼接字符串四部分組成,客戶端發送請求的時候需要帶上對應的身份ID、時間戳、隨機數和計算出的簽名。
服務端過濾器攔截請求,驗證請求參數的合法性、是否過期,Token令牌是否合法、是否過期,全部通過後重新計算簽名,與傳遞的簽名參數對比,一致則執行對應的Api請求,否則返回錯誤消息。如果服務端計算的簽名與傳遞的參數簽名不一樣,請求不合法(可能被篡改),為什麼這麼說呢,因為客戶端與服務端擁有相同的簽名計算方式,如果請求被修改,那麼服務端計算的簽名肯定與客戶端計算的簽名肯定不一致。
1.1 客戶端請求Token令牌流程
客戶端請求Token的憑證是對應的身份ID,當然可以是其他的,這裡假設用的是身份ID。
(1)首先客戶端向服務端發送獲取Token令牌的請求,這個Token令牌是一個GUID碼,生成後服務端會將其存在緩存中,當再次請求的時候會先從緩存中查找。請求Token令牌的代碼:
public static ProductResultMsg.TokenResultMsg GetSignToken(int staffId)
{
string tokenApi = AppSettingsConfig.GetTokenApi;
Dictionary<string, string> parames = new Dictionary<string, string>();
parames.Add("staffid", staffId.ToString());
Tuple<string, string> parameters = GetQueryString(parames);
ProductResultMsg.TokenResultMsg token = WebApiHelper.Get<ProductResultMsg.TokenResultMsg>
(tokenApi, parameters.Item1, staffId.ToString(), staffId, false);
return token;
}
代碼解釋:
1.tokenApi是配置在webConfig中的接口Url
2.Parames字典對象用來封裝參數的,因為請求Token時可能不止一個參數。
3.Parameter:是元組類型,元組可以承載任何的數據類型,這裡用來接收GetQueryString方法返回的拼接字符串
4.token:客戶端用來承載接口返回的Token令牌的類的實例,TokenResultMSg結構如下:
public class TokenResultMsg : HttpResponseMsg
{
public Tokens Result
{
get
{
if (StatusCode == (int)StatusCodeEnum.Success)
{
return JsonConvert.DeserializeObject<Tokens>(Data.ToString());
}
return null;
}
}
}
可以看到這個類實際是封裝了一個Token類實例,因為Api接口返回的是Json數據類型,所以這裡進行了反序列化。Token類結構在最上面,裡面包含身份ID、 Token令牌、 過期時間三個屬性,最後返回包含Token令牌的Token類給主程序。Token類結構如下:
public class Tokens
{
/// <summary>
/// 用戶名
/// </summary>
public int StaffId { get; set; }
/// <summary>
/// 用戶名對應簽名Token
/// </summary>
public Guid SignToken { get; set; }
/// <summary>
/// Token過期時間
/// </summary>
public DateTime ExpireTime { get; set; }
}
(2)上面調用了GetQueryString方法,這個方法是用來整理參數以及參數的拼接字符串,參數用來隨URL傳遞,參數的拼接字符串用來生成對應的Sign簽名,方法如下:
public static Tuple<string, string> GetQueryString(Dictionary<string, string> parames)
{
//第一步:把字典按key的字母順序排序
IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parames);
IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();
//第二部 把所有的名字和參數值串在一起
StringBuilder query = new StringBuilder("");//簽名字符串
StringBuilder queryStr = new StringBuilder("");//url參數
if (parames == null || parames.Count == 0)
{
return new Tuple<string, string>("", "");
}
while (dem.MoveNext())
{
string key = dem.Current.Key;
string value = dem.Current.Value;
if (!string.IsNullOrEmpty(key))
{
query.Append(key).Append(value);
queryStr.Append("&").Append(key).Append("=").Append(value);
}
}
return new Tuple<string, string>(query.ToString(), queryStr.ToString().Substring(1, queryStr.Length - 1));
}
(3)此時Url以及參數已經准備完成,之後調用了 Get 方法,發送獲取Token的請求到WepApi接口,這裡描述一下思路,請求的Get方法你可以做成單獨的,也可以做成公共的,公共的是什麼意思呢,就是這個Get方法不止可以用來請求Token令牌,請求的參數視安全程度決定,這裡為了演示只傳遞了一個身份ID,在實際的操作過程中可以傳遞更多的驗證數據,Get方法代碼如下:(對於後台請求Api接口的方式這裡就不做詳解了)
public static T Get<T>(string webApi, string query, string queryStr, int staffId, bool sign = true)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(webApi + "/" + queryStr);
string timeStamp = GetTimeStamp();
string nonce = GetRandom();
//加入頭信息
request.Headers.Add("staffid", staffId.ToString());//當前請求用戶的Staffid
request.Headers.Add("timestamp", timeStamp);//發起請求的時間戳(單位:毫秒)
request.Headers.Add("nonce", nonce);//發起請求的隨機數
if (sign)
request.Headers.Add("signature", GetSignature(timeStamp, nonce, staffId, query));//當前請求內容的數字簽名
request.Method = "GET";
request.ContentType = "application/json";
request.Timeout = 90000;
request.Headers.Set("Pragma", "no-cache");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream streamReceive = response.GetResponseStream();
StreamReader streamReader = new StreamReader(streamReceive, Encoding.UTF8);
string strResult = streamReader.ReadToEnd();
streamReader.Close();
streamReceive.Close();
request.Abort();
response.Close();
return JsonConvert.DeserializeObject<T>(strResult);
}
方法中有兩個地方需要在這裡說一下,因為博主當時第一次看的時候沒注意,那就是時間戳和隨機數的生成,代碼如下:
/// 獲得時間戳
/// </summary>
/// <returns></returns>
private static string GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalMilliseconds).ToString();
}
/// <summary>
/// 獲取隨機數
/// </summary>
/// <returns></returns>
private static string GetRandom()
{
Random rd = new Random(DateTime.Now.Millisecond);
int i = rd.Next(0, int.MaxValue);
return i.ToString();
}
(4)當代碼執行到HttpWebResponse response = (HttpWebResponse)request.GetResponse();時客戶端發送請求發到Api接口,此時我們來看Api接口收到請求的處理程序:
public class ServiceController : ApiController
{
[HttpGet]
public HttpResponseMessage GetToken(string id)
{
string staffId = id;
ResultMsg resultMsg = null;
int ID = 0;
//判斷參數是否合法
if (string.IsNullOrEmpty(staffId) || (!int.TryParse(staffId, out ID)))
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();
resultMsg.Data = "";
string returnErrJson = Newtonsoft.Json.JsonConvert.SerializeObject(resultMsg);
return new HttpResponseMessage { Content = new StringContent(returnErrJson, System.Text.Encoding.UTF8) };
}
//插入緩存
Tokens token = (Tokens)HttpRuntime.Cache.Get(id.ToString());
if (HttpRuntime.Cache.Get(id.ToString()) == null)
{
token = new Tokens();
token.StaffId = ID;
token.SignToken = Guid.NewGuid();
token.ExpireTime = DateTime.Now.AddDays(1);
HttpRuntime.Cache.Insert(token.StaffId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero);
}
//返回Token信息
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.Success;
resultMsg.Info = "";
resultMsg.Data = token;
string returnJson = Newtonsoft.Json.JsonConvert.SerializeObject(resultMsg);
return new HttpResponseMessage { Content = new StringContent(returnJson, System.Text.Encoding.UTF8) };
}
代碼分析:
服務端收到請求後,接受傳過來的身份ID,首先判斷ID是否為空以及是否合法以及其它一些驗證,如果有什麼地方不合法,就返回錯誤信息到客戶端。
如果身份ID符合所有驗證,則根據ID去緩存中查詢對應的Token令牌,如果緩存中沒有,則新生成對應身份ID的Token令牌,令牌是一段GUID碼,生成之後存入緩存中。
最後,返回一個包含Token的 ResultMsg實體,記得轉換為Json數據類型,ResultMsg類結構如下,Data屬性是object類型,用來承載Token類對象:
public class ResultMsg
{
/// <summary>
/// 狀態碼
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// 操作信息
/// </summary>
public string Info { get; set; }
/// <summary>
/// 返回數據
/// </summary>
public object Data { get; set; }
}
到此,Token令牌就返回到了客戶端,這個過程的執行狀態可以根據實際情況決定,比如是請求一次之後將Token令牌存在客戶端,或者是每次發送請求到客戶端都需要請求一次Token令牌。
1.2 發送數據請求完整描述
在客戶端得到Token令牌之後每次請求都需要帶上它,當然,只有Token令牌還是不夠的,還需要編碼的簽名。簽名由4個部分組成:時間戳、隨機數、Token令牌、數據參數,編碼的方式如下,首先將這四部分進行拼接字符串,然後將字符串中字符按照升序排序,之後轉換為二進制數據流,然後進行MD5哈希加密,MD5是哈希加密的一種,接著循環遍歷加密後的二進制字節流,這個時候字節流的長度時128位的,為了使用方便和節約網絡傳輸流量我們需要把它轉化為16進制的字符串,最後將所有的字符串轉換為大寫。至此,加密簽名完成。生成Signature簽名的代碼如下:
public static string GetSignature(string timeStamp, string nonce, int staffId, string data)
{
Tokens token = null;
var resultMsg = GetSignToken(staffId);
if (resultMsg != null)
{
if (resultMsg.StatusCode == (int)StatusCodeEnum.Success)
{
token = resultMsg.Result;
}
else
{
throw new Exception(resultMsg.Data.ToString());
}
}
else
{
throw new Exception("token為null,員工編號為:" + staffId);
}
var hash = System.Security.Cryptography.MD5.Create();
//拼接簽名數據
var signStr = timeStamp + nonce + staffId + token.SignToken.ToString() + data;
//將字符串中的字符按升序排序
var sortStr = string.Concat(signStr.OrderBy(c => c));
var bytes = Encoding.UTF8.GetBytes(sortStr);
//使用MD5加密
var md5Val = hash.ComputeHash(bytes);
//把二進制轉化為大寫的十六進制
StringBuilder result = new StringBuilder();
foreach (var v in md5Val)
{
result.Append(v.ToString("X2"));
}
return result.ToString().ToUpper();
}
在發送數據請求的時候,需要傳遞四個參數,時間戳(用來判斷請求是否過期)、隨機數(用來強化請求的安全性)、身份ID(服務端用來查詢對應的Token令牌)、Signature簽名。
那麼服務端應該怎麼驗證用戶的請求是否合法呢?服務端通過一個全局的過濾器(Filter)來攔截所有的客戶端請求(不包含請求Token令牌),過濾器攔截到請求後首先判斷請求方式,根據不同的請求方式獲取請求中所有的參數(比如Get是QueryString,Post是InputStream),然後通過參數名稱(key)得到參數的值(value),然後對參數進行相應的驗證(是否為空或null),通過TimeSpan時間戳判斷請求是否過期,如果過期則返回對應的錯誤信息;通過身份ID查詢緩存中的Token令牌,與參數中令牌對比是否正確,如果驗證都通過則對最後的簽名做驗證。
服務端驗證簽名的方式是這樣的,使用與客戶端計算簽名同樣的算法,重新計算簽名字符串,然後與請求中的前民字符串做對比,這裡有個問題是,我在注冊為全局過濾器的時候,系統提示需要實現System.Web.Mvc中四個接口中的一個,但是原文中實現的是System.Web.Http命名空間下的標簽類,博主有點迷糊了,如果有懂得園友大牛可以指點一下,服務端過濾器代碼如下:
public class ApiSecurityFilter:ActionFilterAttribute,IActionFilter
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
ResultMsg resultMsg = null;
var request = actionContext.Request;
string method = request.Method.Method;
string staffid = String.Empty, timestamp = string.Empty, nonce = string.Empty, signature = string.Empty;
int id = 0;
if (request.Headers.Contains("staffid"))
{
staffid = HttpUtility.UrlDecode(request.Headers.GetValues("staffid").FirstOrDefault());
}
if (request.Headers.Contains("timestamp"))
{
timestamp = HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault());
}
if (request.Headers.Contains("nonce"))
{
nonce = HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault());
}
if (request.Headers.Contains("signature"))
{
signature = HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault());
}
//GetToken方法不需要進行簽名驗證
if (actionContext.ActionDescriptor.ActionName == "GetToken")
{
if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id)) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce))
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
return;
}
else
{
base.OnActionExecuting(actionContext);
}
}
//判斷請求頭是否包含以下參數
if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id)) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
//判斷timespan是否有效
double ts1 = 0;
double ts2 = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;
bool timespanvalidate = double.TryParse(timestamp, out ts1);
double ts = ts2 - ts1;
bool falg = ts > int.Parse(WebSettingsConfig.UrlExpireTime) * 1000;
if (falg || (!timespanvalidate))
{
//此時說明時間戳已過期
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.URLExpireError;
//錯誤信息
resultMsg.Info = StatusCodeEnum.URLExpireError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
//判斷Token是否有效
Tokens token = (Tokens)HttpRuntime.Cache.Get(id.ToString());
string signtoken = string.Empty;
if (HttpRuntime.Cache.Get(id.ToString()) == null)
{
//說明身份ID對應的token令牌不存在
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.TokenInvalid;
resultMsg.Info = StatusCodeEnum.TokenInvalid.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
else
{
//緩存中存在ID對應的 Token
signtoken = token.SignToken.ToString();
}
//根據請求類型拼接參數
NameValueCollection form = HttpContext.Current.Request.QueryString;
string data = string.Empty;
switch (method)
{
case "POST":
Stream stream = HttpContext.Current.Request.InputStream;
string responseJson = string.Empty;
StreamReader streamReader = new StreamReader(stream);
data = streamReader.ReadToEnd();
break;
case "GET":
//第一步:取出所有的get參數
IDictionary<string, string> parameters = new Dictionary<string, string>();
for (int f = 0; f < form.Count; f++)
{
string key = form.Keys[f];
parameters.Add(key, form[key]);
}
//第二步 把字典Key的字母順序排序
IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters);
IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();
//第三部:把所有參數名和參數值串在一起
StringBuilder query = new StringBuilder();
while (dem.MoveNext())
{
string key = dem.Current.Key;
string value = dem.Current.Value;
if (!string.IsNullOrEmpty(key))
{
query.Append(key).Append(value);
}
}
data = query.ToString();
break;
default://兩者都不是返回錯誤信息
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.HttpMehtodError;
resultMsg.Info = StatusCodeEnum.HttpMehtodError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
bool result = SignExtension.Validate(timestamp, nonce, id, signtoken, data, signature);
if (!result)
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.HttpRequestError;
resultMsg.Info = StatusCodeEnum.HttpRequestError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
else
{
base.OnActionExecuting(actionContext);
}
}
}
如果服務端與客戶端簽名也一致,所有驗證通過,根據請求執行對應的Api方法,返回結果。這裡就不寫代碼了,再次奉上原文大牛連接:
http://www.cnblogs.com/MR-YY/p/5972380.html#!comments
如果你覺得有幫助,請給原文大牛一個推薦,謝謝。