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

























