Web APi之認證(Authentication)兩種實現方式【二】(十三)
前言
上一節我們詳細講解了認證及其基本信息,這一節我們通過兩種不同方式來實現認證,並且分析如何合理的利用這兩種方式,文中涉及到的基礎知識,請參看上一篇文中,就不再敘述廢話。
序言
對於所謂的認證說到底就是安全問題,在Web API中有多種方式來實現安全,【accepted】方式來處理基於IIS的安全(通過上節提到的WindowsIdentity依賴於HttpContext和IIS認證)或者在Web API裏通過使用Web API中的消息處理機制,但是如果我們想應用程序運行在IIS之外此時Windows Idenitity這一方式似乎就不太可能了,同時在Web API中本身就未提供如何處理認證的直接方式,我們不得不自定義來實現認證功能,同時這也是我們所推薦的方式,自己動手,豐衣足食。
溫馨提示:下面實現方法皆基於基礎認證,若不熟悉Http協議中的Basic基礎認證,請先參看此篇文章【園友海鳥-介紹Basic基礎認證和Digest摘要認證】。
無論何種方式,對於我們的應用程序我們都需要在業務層使用基於憑證的用戶認證,因為是客戶端一方的需求,所以客戶端需要明確基礎驗證,基礎認證(Basic)非常簡單並且支持任何Web客戶端,但是基礎驗證的缺點是不安全,通過使用SSL則可以進行加密就可以在一定程度上保證了安全,如果是對於一般的應用程序通過基礎認證只是進行編碼而未加密也可以說是安全的。我們還是看看上一節所給圖片
通過上述圖片的粗略信息我們可以看出在請求到Action方法之間要經過Web API消息處理管道,在請求到目標元素之前要經過HttpMessageHandler和認證過濾器,所以我們可以通過這兩者來自定義實現認證。下面我們一一來看。
基於Web API的認證過濾器(AuthorizationFilterAttribute)實現認證
第一步
我們自定義一個認證身份(用戶名和密碼)的類,那麽此類必須也就要繼承於 GenericIdentity ,既然是基於基礎驗證,那麽類型當然也就是Basic了。
public class BasicAuthenticationIdentity : GenericIdentity { public string Password { get; set; } public BasicAuthenticationIdentity(string name, string password) : base(name, "Basic") { this.Password = password; } }
第二步
我們要自定義一個認證過濾器特性,並繼承 AuthorizationFilterAttribute ,此時會變成如下:
public class BasicAuthenticationFilter : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) {} }
那麽在這個重寫的方法我們應該寫什麽呢?我們慢慢來分析!請往下看。
-
解析請求報文頭
首先對於客戶端發送過來的請求我們肯定是需要獲得請求報頭,然後解析請求報頭中的Authorization,若此時其參數為空,我們將返回到客戶端,並發起質詢。
string authParameter = null; var authValue = actionContext.Request.Headers.Authorization; //actionContext:Action方法請求上下文 if (authValue != null && authValue.Scheme == "Basic") authParameter = authValue.Parameter; //authparameter:獲取請求中經過Base64編碼的(用戶:密碼) if (string.IsNullOrEmpty(authParameter)) return null;
次之,若此時認證中的參數不為空並開始對其進行解碼,並返回一個BasicAuthenticationIdentity對象,若此時對象為空,則同樣返回到客戶端,並發起質詢
authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); //對編碼的參數進行解碼 var authToken = authParameter.Split(‘:‘); //解碼後的參數格式為(用戶名:密碼)將其進行分割 if (authToken.Length < 2) return null; return new BasicAuthenticationIdentity(authToken[0], authToken[1]); //將分割的用戶名和密碼傳遞給此類構造函數進行初始化
最後,我們將上述兩者封裝為一個ParseHeader方法以便進行調用
public virtual BasicAuthenticationIdentity ParseHeader(HttpActionContext actionContext) { string authParameter = null; var authValue = actionContext.Request.Headers.Authorization; if (authValue != null && authValue.Scheme == "Basic") authParameter = authValue.Parameter; if (string.IsNullOrEmpty(authParameter)) return null; authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); var authToken = authParameter.Split(‘:‘); if (authToken.Length < 2) return null; return new BasicAuthenticationIdentity(authToken[0], authToken[1]); }
-
接下來我們將認證未通過而需要發起認證質詢,我們將其封裝為一個方法Challenge
void Challenge(HttpActionContext actionContext) { var host = actionContext.Request.RequestUri.DnsSafeHost; actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); actionContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", host)); }
-
定義一個方法便於對用戶名和密碼進行校驗,並將其修飾為虛方法,以免後續要添加其他有關用戶數據
public virtual bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext) { if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword)) return false; else return true; }
-
在認證成功後將認證身份設置給當前線程中Principal屬性
var principal = new GenericPrincipal(identity, null); Thread.CurrentPrincipal = principal; //下面是針對ASP.NET而設置 //if (HttpContext.Current != null) // HttpContext.Current.User = principal;
第三步
一切已經就緒,此時在重寫方法中進行相應的調用即可,如下:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] public class BasicAuthenticationFilter : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { var userIdentity = ParseHeader(actionContext); if (userIdentity == null) { Challenge(actionContext); return; } if (!OnAuthorize(userIdentity.Name, userIdentity.Password, actionContext)) { Challenge(actionContext); return; } var principal = new GenericPrincipal(userIdentity, null); Thread.CurrentPrincipal = principal; base.OnAuthorization(actionContext); }
第四步
自定義 CustomBasicAuthenticationFilter 並繼承於 BasicAuthenticationFilter ,重寫其虛方法。
public class CustomBasicAuthenticationFilter : BasicAuthenticationFilter { public override bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext) { if (userName == "xpy0928" && userPassword == "cnblogs") return true; else return false; } }
最後一步
註冊自定義認證特性並進行調用
config.Filters.Add(new CustomBasicAuthenticationFilter()); [CustomBasicAuthenticationFilter] public class ProductController : ApiController {....}
至此對於其認證方式就已經完全實現,接下來我們通過【搜狗瀏覽器】來驗收我們的成果。
看到如下認證其用戶名和密碼的圖片,我們知道我們成功了一半
我們點擊取消,觀察是否返回401並添加質詢頭即WWW-Authenticate,如我們所料
我們輸入正確的用戶名和密碼再試試看,結果認證成功,如下:
基於Web API的消息處理管道(HttpMessageHandler)實現認證
我們知道HttpMessageHandler是Web API中請求-響應中的消息處理管道的重要角色,但是真正實現管道串聯的是DelegatingHandler,若你不懂Web API消息管道,請參考前面系列文章,所以我們可以自定義管道來進行攔截通過繼承DelegatingHandler。下面我們一步步來實現基於此管道的認證。
第一步
和第一種方法一致不再敘述。
第二步
這一步當然是自定義管道進行處理並繼承DelegatingHandler,重載在此類中的SendAsync方法,通過獲得其請求並處理從而進行響應,若不懂此類中的具體實現,請參看前面系列文章。
-
同樣是我們需要根據請求來解析請求報頭,我們依然需要解析報頭方法,但是需要稍作修改
public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage) { string authParameter = null; var authValue = requestMessage.Headers.Authorization; if (authValue != null && authValue.Scheme == "Basic") authParameter = authValue.Parameter; if (string.IsNullOrEmpty(authParameter)) return null; authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); var authToken = authParameter.Split(‘:‘); if (authToken.Length < 2) return null; return new BasicAuthenticationIdentity(authToken[0], authToken[1]); }
-
此時質詢也得作相應的修改,因為此時不再是依賴於Action請求上下文,而是請求(HttpRequestMessage)和響應(HttpResponseMessage)
void Challenge(HttpRequestMessage request,HttpResponseMessage response) { var host = request.RequestUri.DnsSafeHost; response.Headers.Add(authenticationHeader, string.Format("Basic realm=\"{0}\"", host)); }
-
最終繼承自DelegatingHandler的代碼如下
public class BasicAuthenticationHandler : DelegatingHandler { private const string authenticationHeader = "WWW-Authenticate"; protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var crendentials = ParseHeader(request); if (crendentials != null) { var identity = new BasicAuthenticationIdentity(crendentials.Name, crendentials.Password); var principal = new GenericPrincipal(identity, null); Thread.CurrentPrincipal = principal; //針對於ASP.NET設置 //if (HttpContext.Current != null) // HttpContext.Current.User = principal; } return base.SendAsync(request, cancellationToken).ContinueWith(task => { var response = task.Result; if (crendentials == null && response.StatusCode == HttpStatusCode.Unauthorized) { Challenge(request, response); } return response; }); } void Challenge(HttpRequestMessage request,HttpResponseMessage response) { var host = request.RequestUri.DnsSafeHost; response.Headers.Add(authenticationHeader, string.Format("Basic realm=\"{0}\"", host)); } public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage) { string authParameter = null; var authValue = requestMessage.Headers.Authorization; if (authValue != null && authValue.Scheme == "Basic") authParameter = authValue.Parameter; if (string.IsNullOrEmpty(authParameter)) return null; authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); var authToken = authParameter.Split(‘:‘); if (authToken.Length < 2) return null; return new BasicAuthenticationIdentity(authToken[0], authToken[1]); } }
第三步
上述我們自定義的BasicAuthenticationFilter此時就得繼承 AuthorizeAttribute 該特性也是繼承於上述的 AuthorizationFilterAttribute ,我們需要利用AuthorizeAttribute中的 IsAuthorized 方法來驗證當前線程中的Principal是否已經被授權。
public class BasicAuthenticationFilter : AuthorizeAttribute { protected override bool IsAuthorized(HttpActionContext actionContext) { var identity = Thread.CurrentPrincipal.Identity; if (identity != null && HttpContext.Current != null) identity = HttpContext.Current.User.Identity; if (identity != null && identity.IsAuthenticated) { var basicAuthIdentity = identity as BasicAuthenticationIdentity;
//可以添加其他需要的業務邏輯驗證代碼 if (basicAuthIdentity.Name == "xpy0928" && basicAuthIdentity.Password == "cnblogs") { return true; } } return false; } }
通過 IsAuthorized 方法返回值來看,若為false,則返回401狀態碼,此時會觸發 BasicAuthenticationHandler 中的質詢,並且此方法裏面主要是我們需要添加認證用戶的業務邏輯代碼。同時我們也說過我們第一種方法自定義實現的過濾器特性是 AuthorizationFilterAttribute (如果我們有更多邏輯使用這個特性是個不錯的選擇),而在這裏是 AuthorizeAttribute (對於驗證用戶並且返回bool值使用此過濾器特性是個不錯的選擇)。
第四步
註冊自定義管道以及認證過濾器特性
config.MessageHandlers.Add(new BasicAuthenticationHandler()); config.Filters.Add(new BasicAuthenticationFilter());
最後一步
[BasicAuthenticationFilter] public class ProductController : ApiController {.....}
下面我們通過【360極速瀏覽器】來驗收成果。點擊按鈕直接請求控制器
接下來取消,是否返回401
至此完美結束。
總結
用認證特性(AuthorizationFilterAttribute)還是HttpMessageHandler實現認證,這是一個問題?
通過比較這二者的實現操作在實現方式上明顯有極大的不同,個人覺得用AuthorizationFilterAttribute來實現認證是更加簡單並且緊湊,因為實現的每一處都在每一個地方,在大多數實現自定義登陸的場景下,對於用過濾器如此緊湊的業務邏輯用這個更加高效, 用HttpMessageHandler的優點是全局應用且是Web API消息處理管道的一部分,如果對於不同的部分要用不同的認證那麽用HttpMessageHandler效果更好,但是此時你需要自定義一個過濾器,尤其是當MessageHandler對於一個認證需要一個過濾器的時候。所以綜上所述,根據不同的應用場景我們應該選擇對應的方式來實現認證。
源代碼鏈接
WebAPi之認證.7z
Web APi之認證(Authentication)兩種實現方式【二】(十三)