以前簡單介紹過web api 的設計,但是還是有很多朋友問我,如何合理的設計和實作web api。比如,接口安全,異常處理,統一資料傳回等問題。是以有必要系統的總結總結 web api 的設計和實作。由于前面已經介紹過web api 的參數和傳回格式的設計,《Web API系列(一)設計經驗與總結》。這次,就來講講接口安全。
由于Web API是基于網際網路的應用,是以安全性要遠比在本地通路資料庫的要嚴格的多,一般通用的做法,是采用幾步來保證接口和資料安全:
1.首先一個是基于CA憑證的HTTPS進行資料傳輸,防止資料被竊聽;
2.然後是采用參數加密簽名方式傳遞,對傳遞的參數,增加一個加密簽名,在伺服器端驗證簽名内容,防止被篡改;
3.最後是對一般的接口通路,都需要使用使用者身份的token進行校驗,隻要檢查通過才允許通路資料。
Web API接口的通路方式,大概可以分為幾類:
1)使用使用者名密碼。這種方式比較簡單,可以有效識别使用者的身份(如包括使用者資訊、密碼、或者相關的接口權限等等)。驗證成功後,傳回相關的資料。
2)使用安全簽名。這種方式送出的資料,URL連接配接的簽名參數是經過安全一定規則的加密的,伺服器收到資料後也經過同樣規則的安全加密,确認資料沒有被中途篡改後,再進行資料修改處理。是以我們可以為不同用戶端,如Web/APP/Winfrom等不同接入方式指定不同的加密秘鑰,但是秘鑰是雙方約定的,并不在網絡連接配接上傳輸,連接配接傳輸的一般是這個接入的AppID,伺服器通過這個AppID來進行簽名參數的加密對比。目前微信背景的回調處理機制,應該就是這麼處理的。
3)公開的接口調用,不需要傳入使用者令牌、或者對參數進行加密簽名的,這種接口一般較少,隻是提供一些很正常的資料顯示而已。
web api 安全校驗
使用使用者名密碼的實作方式比較簡單,這裡就不說明如何實作了。就講一講安全簽名的實作。由于Web API的調用,都是一種無狀态的調用方式,所有的接口請求,都要帶安全簽名。
web api核心安全校驗代碼片斷:
public class QueryData
{
public QueryData()
{
}
public QueryData(IEnumerable<KeyValuePair<string, string>> paramList)
{
// TODO: Complete member initialization
try
{
if (paramList == null)
{
throw new Exception("請求參數為空!");
}
foreach (var param in paramList)
{
m_values[param.Key] = param.Value; //
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
//采用排序的Dictionary的好處是友善對資料包進行簽名,不用再簽名之前再做一次排序
private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();
/**
* 設定某個字段的值
* @param key 字段名
* @param value 字段值
*/
public void SetValue(string key, object value)
{
m_values[key] = value;
}
/**
* 根據字段名擷取某個字段的值
* @param key 字段名
* @return key對應的字段值
*/
public object GetValue(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
return o;
}
/**
* 判斷某個字段是否已設定
* @param key 字段名
* @return 若字段key已被設定,則傳回true,否則傳回false
*/
public bool IsSet(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
if (null != o)
return true;
else
return false;
}
public string ToUrl()
{
string buff = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
throw new Exception("内部含有值為null的字段!");
}
if (pair.Key != "sign" && pair.Value.ToString() != "")
{
buff += pair.Key + "=" + pair.Value + "&";
}
}
buff = buff.Trim('&');
return buff;
}
public string MakeSign(string appKey = "test")
{
//轉url格式
string str = ToUrl();
//在string後加入API KEY
str += "&key=" + appKey;
//MD5加密
var md5 = MD5.Create();
var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
var sb = new StringBuilder();
foreach (byte b in bs)
{
sb.Append(b.ToString("x2"));
}
//所有字元轉為大寫
return sb.ToString().ToUpper();
}
public bool CheckSign()
{
//如果沒有設定簽名,則跳過檢測
if (!IsSet("sign"))
{
throw new Exception("簽名存在但不合法!");
}
//如果設定了簽名但是簽名為空,則抛異常
else if (GetValue("sign") == null || GetValue("sign").ToString() == "")
{
throw new Exception("簽名存在但不合法!");
}
//擷取接收到的簽名
string return_sign = GetValue("sign").ToString();
//在本地計算新的簽名
string cal_sign = MakeSign();
if (cal_sign == return_sign)
{
return true;
}
return false;
}
}