接口參數簽名校驗,是WebApi接口服務最重要的安全防禦手段之一. 結合項目中實際使用狀況,介紹下先後端參數簽名校驗實現方案。javascript
http請求,有兩種傳參形式:html
1.經過url傳參,最多見的就是get請求(實際上post,put,delete均可以使用這種傳參方式),如:前端
http://api.XXX.com/getproduct?id=value1vue
2.經過request body傳參,最多見的就是post請求,以下圖所示
咱們針對於以上兩種傳參方式,採用不一樣的簽名校驗規則(注:簽名算法規則僅供參考)。WebApi是不支持經過url和body同時傳參數的,因此在服務端能夠經過HttpContext.Current.Request.QueryString 獲取到form參數進行判斷,執行不一樣邏輯,以下代碼所示:java
var form = HttpContext.Current.Request.QueryString; // 請求的url參數 var data = string.Empty; if (form.Count > 0) { //第一步:取出全部form參數 IDictionary<string, string> parameters = new Dictionary<string, string>(); for (var f = 0; f < form.Count; f++) { var key = form.Keys[f]; parameters.Add(key, form[key]); } // 第二步:把字典按Key的字母順序排序 IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters); var dem = sortedParams.GetEnumerator(); // 第三步:把全部參數名和參數值串在一塊兒 var query = new StringBuilder(); while (dem.MoveNext()) { var key = dem.Current.Key; var value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) query.Append(key).Append(value); } data = query.ToString(); } else { //請求輸入的內容,即body內容 var stream = HttpContext.Current.Request.InputStream; stream.Position = 0; var responseJson = string.Empty; var streamReader = new StreamReader(stream); data = streamReader.ReadToEnd(); stream.Position = 0; }
經過上述邏輯以後,data變量中存儲的就是接口參數內容。 如下是實際簽名校驗邏輯ios
/// <summary> /// 簽名校驗 /// </summary> /// <param name="timeStamp">時間戳(按秒)</param> /// <param name="data">參數內容</param> /// <param name="signature">前端簽名值</param> /// <returns></returns> public bool Validate(string timeStamp, string data, string signature) { var hash = MD5.Create(); //拼接簽名數據 var signStr = timeStamp + data; //將字符串中字符按升序排序 var sortStr = string.Concat(signStr.OrderBy(c => c)); var bytes = Encoding.UTF8.GetBytes(sortStr); //使用32位大寫 MD5簽名 var md5Val = hash.ComputeHash(bytes); var result = new StringBuilder(); foreach (var c in md5Val) result.Append(c.ToString("X2")); var s = result.ToString().ToUpper(); //與前端傳過來的簽名參數進行比對 return s == signature; }
建立WebApi的Action攔截器HandlerSecretAttributegit
/// <summary> /// 簽名安全攔截過濾器 /// </summary> public class HandlerSecretAttribute : ActionFilterAttribute { private readonly ExcuteMode _customMode; /// <summary>默認構造</summary> /// <param name="Mode">認證模式</param> public HandlerSecretAttribute(ExcuteMode Mode) { _customMode = Mode; } /// <summary> /// 安全校驗 /// </summary> /// <param name="filterContext"></param> public override void OnActionExecuting(HttpActionContext filterContext) { //是否忽略權限驗證 if (_customMode == ExcuteMode.Ignore) return; //從http請求的頭裏面獲取AppId var request = filterContext.Request; var method = request.Method.Method; var appId = ""; //客戶端應用惟一標識 long timeStamp; //時間戳, 校驗10分鐘內有效 var signature = ""; //參數簽名,去除空參數,按字母倒序排序進行Md5簽名 爲了提升傳參過程當中,防止參數被惡意修改,在請求接口的時候加上sign能夠有效防止參數被篡改 try { appId = request.Headers.GetValues("appId").SingleOrDefault(); timeStamp = Convert.ToInt64(request.Headers.GetValues("timeStamp").SingleOrDefault()); signature = request.Headers.GetValues("signature").SingleOrDefault(); } catch (Exception ex) { throw new UserFriendlyException("簽名參數異常:" + ex.Message); } #region 安全校驗 //TODO:實際邏輯處理 base.OnActionExecuting(filterContext); #endregion } }
具體的使用,以下圖所示:github
若是是須要全局註冊,請在WebApi.config中配置,以下圖所示:web
有關HandlerSecretAttribute的源代碼,我已整理放至github上https://github.com/yinboxie/BlogExampleDemoajax
客戶端http請求(以Axios爲例)進行統一的攔截處理。前端用過諸多http插件,如ajax,fetch,vue-resoure,axios等,我的感受axios的請求攔截是最好用的。
import axios from 'axios' import { sign } from './sign' let _ = require('lodash') var service = axios.create({ baseURL:'http://xxx.com' timeout:10000 // 請求超時時間 }) // request攔截器 service.interceptors.request.use( config => { let token = getToken() if (token) { config.headers['token'] = token // 讓每一個請求攜帶自定義token 請根據實際狀況自行修改 } // 若是接口須要簽名, 則經過請求時,headers中傳遞sign參數true if (config.headers['sign']) { config = sign(config) // 核心簽名邏輯,獨立封裝了處理函數 } return config }, error => { // Do something with request error console.log(error) // for debug Promise.reject(error) } )
sign函數核心源碼
import md5 from 'js-md5' let _ = require('lodash') /** * 接口參數簽名 * @param {*} config 請求配置 */ export const sign = config => { // 獲取到秒級的時間戳,與後端對應 let tmp = new Date() .getTime() .toString() .substr(0, 10) let header = { appId:'pmes', timeStamp: tmp, signature: '' } let signStr = _.toString(header.timeStamp) if (config.params) { // url參數簽名 let pArray = [] for (let p in config.params) { pArray.push(p) } let sArray = pArray.sort() for (let item of sArray) { signStr += item + _.toString(config.params[item]) } } else if (config.data) { // request body參數的內容 signStr += JSON.stringify(config.data) } // 簽名核心邏輯 let newsignStr = _.sortBy(signStr, s => s.charCodeAt(0)).join('') let s = md5(newsignStr).toUpperCase() header.signature = s config = Object.assign(config, { headers: header }) return config }
實際的調用函數
// post請求, body傳參 axios.post('/Login/CheckLoginTest1', { Account: 'xiaowang',Password: '123' }, { headers: { sign: true }} ).then(d => { console.log(d) }) // post請求,url傳參 axios.post('/Login/CheckLoginTest3', null, { params: {t1: '2',t2: '3'}, headers: { sign: true } } ).then(d => { console.log(d) }) // get請求 axios.get('/Login/CheckLoginTest2', { params: {t1: '2',t2: '3'}, headers: { sign: true } } ).then(d => { console.log(d) })
爲了保證WebApi數據在通訊時的安全性,須要採起多重安全防禦: 參數簽名校驗,token驗證,跨域權限,時間戳過時校驗,容許請求的appId校驗等。固然具體的規則咱們都得依據項目實際狀況去實現,這裏就再也不展開討論其餘方式的實現。