在各類手機APP氾濫的如今,背後都有一樣氾濫的API接口在支撐,其中魚龍混雜,直接裸奔的WEB API大量存在,安全性使人堪優面試
在之前WEB API概念沒有很普及的時候,都採用自已定義的接口和結構,對於公開訪問的接口,專業點的都會作下安全驗證,數據簽名之類api
反而如今,誰均可以用WEB API估接口,安全性早忘一邊了,特別是外包小公司的APP項目,80%都有安全漏洞(面試了大半年APP開發得出的結論)安全
特在過年以前,整理了下在用的解決方案,本方案解決了服務器
正文微信
對於一個接口,返回的內容除了要返回業務數據外,還得返回處理狀態,而且這個狀態是在每一個接口都得有數據結構
因此數據格式都會定義爲:app
數據頭(描述數據信息)框架
-----------------------------------ide
數據體(具體數據)工具
本文定義結構爲
/// <summary> /// 處理結果 /// </summary> public class DealResult { /// <summary> /// 處理結果 /// </summary> public bool Result { get; set; } /// <summary> /// 消息 /// </summary> public string Message { get; set; } /// <summary> /// 關聯數據 /// </summary> public object Data { get; set; } }
全部接口都返回此對象,會描述本次請求的狀態,和對應的數據,服務端則根據實際狀況,返回處理結果和對應的數據
開方式接口安全性就不用多說了,解決方法爲加密,或數據簽名驗證,本文方案爲進行數據簽名
同返回的數據同樣,提交到服務器的數據格式也統一約定,定義一個數據頭基類
/// <summary> /// 參數基類 /// </summary> [Serializable] public class ParameBase { string time = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss"); /// <summary> /// 時間 格式 yyyy-MM-dd hh:mm:ss /// </summary> public string Time { get { return time; } set { time = value; } } /// <summary> /// 來源網站 = 1, IOS = 2,Android = 3, 微信 = 4 /// </summary> public int SourceFrom { get; set; } /// <summary> /// 簽名 /// </summary> public string Token { get; set; } }
一個登陸對象表示爲
/// <summary> /// 登陸 /// </summary> public class Login : ParameBase { /// <summary> /// 用戶名 /// </summary> public string Name { get; set; } /// <summary> /// 密碼 /// </summary> public string Password { get; set; } }
數據簽名表示爲(KEY稍後講到)
Token=MD5(屬性值1+值2....+KEY)
按此對象表示爲 MD5(Name+PassWord+Source+Time+KEY)
若是是GET參數怎麼辦,同樣,按參數名計算,同時傳遞的參數要附帶上Source,Time,Token
有的喜歡把密鑰放在客戶端,或固定密鑰,顯然都有安全問題,解決方法是動態獲取
這就意味着在設計接口時,有一個接口是首先要調用的,讓服務器返回密鑰,因而就有了登陸的概念
過程表示爲
登陸>返回用戶信息和密鑰=>存儲用戶信息和密鑰=>使用密鑰調用其它接口
這樣只有登陸者和服務器才知道自已的密鑰了
綜上所述,數據結構表示爲
客戶端提交結構爲 ParameBase(附帶簽名信息)
服務端返回結構爲 DealResult
登陸機制
同網頁請求同樣,怎麼知道屢次調用是同一我的呢,這裏採用了COOKIE的形式,登陸後服務端返回一個COOKIE,客戶端再請求時帶上這個COOKIE
服務端須要存儲這個COOKIE標識,全部的驗證處理都會基於此標識來判斷用戶
有了上面基礎,進入項目階段
其實用什麼項目類型都行,只是WEB API方便了對象結構序列化和傳參
默認WEB API路由RESUFUL形式,沒有控制器方法,只能按METHOD來定義,很不方便,改爲控制器的形式,這樣就能用方法名來訪問了
更改路由配置爲
config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}",//加上路由ACTION參數 defaults: new { id = RouteParameter.Optional } );
在此文,數據分爲請求和返回,以登陸返回用戶信息爲例,登陸爲請求,用戶信息爲返回,示例對象結構爲
用戶對象
/// <summary> /// 登陸返回用戶 /// </summary> public class User { /// <summary> /// 用戶編號 /// </summary> public int Id { get; set; } /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// 本次登陸的KEY /// </summary> public string Key { get; set; } /// <summary> /// 本資登陸的憑證 /// </summary> public string Voucher { get; set; } }
請求方式
這裏只採用了GET,POST兩種方式,根據實際狀況定義,控制器方法必定須要都標明,否則會出現路由BUG
定義登陸方法
/// <summary> /// 登陸 /// </summary> /// <param name="parame"></param> /// <returns>User</returns> [HttpPost] [AnonymousSign] public DealResult Login([FromBody] Login parame) { if (parame.Password != "123") { return DealResult(false, "密碼不正確"); } string key2 = System.Guid.NewGuid().ToString(); string voucher = System.Guid.NewGuid().ToString(); var user = new User() { Name = parame.Name, Id = 1, Key = key2, Voucher = voucher }; var timeDiff = (DateTime.Now - Convert.ToDateTime(parame.Time)).TotalSeconds;//保存客戶端和服務端時間差 LoginStatusContext.SetLoginStatus(voucher, user.Id, key2, timeDiff); CoreHelper.CookieHelper.AddCookies("user", voucher);//存入COOKIE return DealResult(true, "", user); }
這裏能夠看到,建立了兩個GUID,一個爲用戶憑證,一個爲用戶密鑰,放入用戶信息返回,同時調用LoginStatusContext.SetLoginStatus保存登陸信息
同時使用了AnonymousSign標註,此方法使用默認簽名Setting.DefaultKey
定義獲取用信息方法
/// <summary> /// 基本信息 /// </summary> /// <param name="name">參數name</param> /// <returns>User</returns> [HttpGet] public DealResult GetBasicInfo(string name) { var user = new User() { Name = name, Id = CurrentUserId }; return DealResult(true, string.Empty, user); }
示例控制器完整定義
/// <summary> /// 賬號操做 /// </summary> [SignCheckAttribute] public class AccountController : BaseController { /// <summary> /// 登陸 /// </summary> /// <param name="parame"></param> /// <returns>User</returns> [HttpPost] [AnonymousSign] public DealResult Login([FromBody] Login parame) { if (parame.Password != "123") { return DealResult(false, "密碼不正確"); } string key2 = System.Guid.NewGuid().ToString(); string voucher = System.Guid.NewGuid().ToString(); var user = new User() { Name = parame.Name, Id = 1, Key = key2, Voucher = voucher }; var timeDiff = (DateTime.Now - Convert.ToDateTime(parame.Time)).TotalSeconds;//保存客戶端和服務端時間差 LoginStatusContext.SetLoginStatus(voucher, user.Id, key2, timeDiff); CoreHelper.CookieHelper.AddCookies("user", voucher);//存入COOKIE return DealResult(true, "", user); } /// <summary> /// 基本信息 /// </summary> /// <param name="name">參數name</param> /// <returns>User</returns> [HttpGet] public DealResult GetBasicInfo(string name) { var user = new User() { Name = name, Id = CurrentUserId }; return DealResult(true, string.Empty, user); } /// <summary> /// 測試異常 /// </summary> /// <returns></returns> [HttpGet] public DealResult TestException() { int a = 0; var b = 10 / a; return DealResult(true); } }
此控制器標註了SignCheckAttribute用以進行簽名判斷
具體實現可看SignCheckAttribute代碼
SignCheckAttribute裏實現了有
爲了統一處理異常,配置了異常處理
GlobalConfiguration.Configuration.Filters.Add(new ExceptionAttribute());
大殺器來了,配合此方案放出了對應的測試工具,雖然WEB API有個擴展,但無法對此方案測試
使用此工具能方便按方案要求調用接口,爲了方便參數拼接,POST和GET都採用URL參數的形式輸入
測試登陸/api/account/login
測試獲取信息/api/account/GetBasicInfo
測試異常處理/api/account/TestException
在未登陸狀況下調用獲取信息
接口結構文檔一直是很讓人頭疼的事,手寫更改了又得維護,版本不同還麻煩,自動生成最好了,一樣WEB API 帶擴展無法表示此結構詳細
大殺器2號來了,按代碼註釋動態生成接口文檔,文檔格式與控制器保持一致
Home控制器代碼實現
public ActionResult Index(SummaryAnalysis.ExportType exportType = SummaryAnalysis.ExportType.NONE) { if (exportType != SummaryAnalysis.ExportType.NONE) { var str = SummaryAnalysis.Load(exportType); return File(str, "application/octet-stream", "Model_" + exportType + ".zip"); } else { if (string.IsNullOrEmpty(outPut)) { outPut = SummaryAnalysis.Load(exportType); } ViewBag.OutPut = outPut; return View(); } } }
在見過的開發文檔,我以爲這是最好的展示形式了,還有錨點,快速定位到對象結構,而且與源代碼保持一致
附WEB API 自帶文檔生成區別
附上項目源碼
http://pan.baidu.com/s/1c2rDacK
項目結構:
----------WPF測試程序
----------接口示例
雖然跟CRL快速開發框架無關,但仍是加上CRL的名,好文要頂!