WebApi安全性 引數簽名校驗(結合Axios使用)
介面引數簽名校驗,是WebApi介面服務最重要的安全防護手段之一. 結合專案中實際使用情況,介紹下前後端引數簽名校驗實現方案。
簽名校驗規則
http請求,有兩種傳參形式:
1.通過url傳參,最常見的就是get請求(實際上post,put,delete都可以使用這種傳參方式),如:
http://api.XXX.com/getproduct?id=value1
2.通過request body傳參,最常見的就是post請求,如下圖所示
我們針對於以上兩種傳參方式,採用不同的簽名校驗規則(注:簽名演算法規則僅供參考)。WebApi是不支援通過url和body同時傳引數的,所以在服務端可以通過HttpContext.Current.Request.QueryString 獲取到form引數進行判斷,執行不同邏輯,如下程式碼所示:
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變數中儲存的就是介面引數內容。 以下是實際簽名校驗邏輯
/// <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; }
Action攔截器實現對某些Api進行簽名校驗
建立WebApi的Action攔截器HandlerSecretAttribute
/// <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
}
}
具體的使用,如下圖所示:
如果是需要全域性註冊,請在WebApi.config中配置,如下圖所示:
有關HandlerSecretAttribute的原始碼,我已整理放至github上https://github.com/yinboxie/BlogExampleDemo
前端Axios請求統一攔截處理
客戶端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校驗等。當然具體的規則我們都得依據專案實際情況去實現,這裡就不再展開討論其他方式的實現。
參考
WebApi安全性 使用TOKEN+簽名驗