以前前後總結並發表了關於WEB Service、WCF身份驗證相關文章,以下:html
關於WEB Service&WCF&WebApi實現身份驗證之WEB Service篇、web
關於WEB Service&WCF&WebApi實現身份驗證之WCF篇(1)、關於WEB Service&WCF&WebApi實現身份驗證之WCF篇(2)數據庫
今天再來總結關於如何實現WebApi的身份驗證,以完成該系列全部文章,WebApi常見的實現方式有:FORM身份驗證、集成WINDOWS驗證、Basic基礎認證、Digest摘要認證api
第一種:FORM身份驗證(若在ASP.NET應用程序使用,則該驗證方式不支持跨域,由於cookie沒法跨域訪問)跨域
1.定義一個FormAuthenticationFilterAttribute,該類繼承自AuthorizationFilterAttribute,並重寫其OnAuthorization,在該方法中添加從請求頭中獲取有無登陸的Cookie,如有則表示登陸成功,不然失敗,代碼以下:瀏覽器
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Http.Filters; using System.Web.Security; using System.Net.Http; using System.Collections.ObjectModel; using System.Net.Http.Headers; using System.Threading; using System.Security.Principal; using System.Net; using System.Text; namespace WebApplication1.Models { public class FormAuthenticationFilterAttribute : AuthorizationFilterAttribute { private const string UnauthorizedMessage = "請求未受權,拒絕訪問。"; public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext) { if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0) { base.OnAuthorization(actionContext); return; } if (HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated) { base.OnAuthorization(actionContext); return; } var cookies = actionContext.Request.Headers.GetCookies(); if (cookies == null || cookies.Count < 1) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent(UnauthorizedMessage, Encoding.UTF8) }; return; } FormsAuthenticationTicket ticket = GetTicket(cookies); if (ticket == null) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent(UnauthorizedMessage, Encoding.UTF8) }; return; } //這裏能夠對FormsAuthenticationTicket對象進行進一步驗證 var principal = new GenericPrincipal(new FormsIdentity(ticket), null); HttpContext.Current.User = principal; Thread.CurrentPrincipal = principal; base.OnAuthorization(actionContext); } private FormsAuthenticationTicket GetTicket(Collection<CookieHeaderValue> cookies) { FormsAuthenticationTicket ticket = null; foreach (var item in cookies) { var cookie = item.Cookies.SingleOrDefault(c => c.Name == FormsAuthentication.FormsCookieName); if (cookie != null) { ticket = FormsAuthentication.Decrypt(cookie.Value); break; } } return ticket; } } }
2.在須要認證受權後才能訪問的Controller中類或ACTION方法上添加上述受權過濾器FormAuthenticationFilterAttribute,也可在global文件中將該類添加到全局過濾器中,同時定義一個登陸ACTION,用於登陸入口,示例代碼以下:cookie
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web; using System.Web.Http; using System.Web.Security; using WebApplication1.Models; namespace WebApplication1.Controllers { [FormAuthenticationFilter] public class TestController : ApiController { [AllowAnonymous] [AcceptVerbs("Get")] [Route("Api/Test/Login")] public HttpResponseMessage Login(string uname, string pwd) { if ("admin".Equals(uname, StringComparison.OrdinalIgnoreCase) && "api.admin".Equals(pwd)) { //建立票據 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, uname, DateTime.Now, DateTime.Now.AddMinutes(30), false, string.Empty); //加密票據 string authTicket = FormsAuthentication.Encrypt(ticket); //存儲爲cookie HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, authTicket); cookie.Path = FormsAuthentication.FormsCookiePath; HttpContext.Current.Response.AppendCookie(cookie); //或者 //FormsAuthentication.SetAuthCookie(uname, false, "/"); return Request.CreateResponse(HttpStatusCode.OK, "登陸成功!"); } else { HttpContext.Current.Response.AppendCookie(new HttpCookie(FormsAuthentication.FormsCookieName) { Expires = DateTime.Now.AddDays(-10) });//測試用:當登陸失敗時,清除可能存在的身份驗證Cookie return Request.CreateErrorResponse(HttpStatusCode.NotFound, "登陸失敗,無效的用戶名或密碼!"); } } // GET api/test public IEnumerable<string> GetValues() { return new string[] { "value1", "value2" }; } // GET api/test/5 public string GetValue(int id) { return "value"; } } }
測試用法一:可直接在瀏覽器中訪問須要受權的方法(即:Login除外),如:http://localhost:11099/api/test/,響應結果以下:併發
請求頭信息以下:async
若成功調用Login方法後(http://localhost:11099/api/test/login?uname=admin&pwd=api.admin),再調用上述方法,則能夠得到正常的結果,以下圖示:ide
看一下請求時附帶的Cookie,以下圖示:
測試用法二:採用HttpClient來調用Api的相關方法,示例代碼以下:
public async static void TestLoginApi() { HttpClientHandler handler = new HttpClientHandler(); handler.UseCookies = true;//由於採用Form驗證,因此須要使用Cookie來記錄身份登陸信息 HttpClient client = new HttpClient(handler); Console.WriteLine("Login>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); var response = await client.GetAsync("http://localhost:11099/api/test/login/?uname=admin&pwd=api.admin"); var r = await response.Content.ReadAsAsync<dynamic>(); Console.WriteLine("StatusCode:{0}", response.StatusCode); if (!response.IsSuccessStatusCode) { Console.WriteLine("Msg:{1}", response.StatusCode, r.Message); return; } Console.WriteLine("Msg:{1}", response.StatusCode, r); var getCookies = handler.CookieContainer.GetCookies(new Uri("http://localhost:11099/")); Console.WriteLine("獲取到的cookie數量:" + getCookies.Count); Console.WriteLine("獲取到的cookie:"); for (int i = 0; i < getCookies.Count; i++) { Console.WriteLine(getCookies[i].Name + ":" + getCookies[i].Value); } Console.WriteLine("GetValues>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); response = await client.GetAsync("http://localhost:11099/api/test/"); var r2 = await response.Content.ReadAsAsync<IEnumerable<string>>(); foreach (string item in r2) { Console.WriteLine("GetValues - Item Value:{0}", item); } Console.WriteLine("GetValue>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); response = await client.GetAsync("http://localhost:11099/api/test/8"); var r3 = await response.Content.ReadAsAsync<string>(); Console.WriteLine("GetValue - Item Value:{0}", r3); }
結果以下圖示:
若是Web Api做爲ASP.NET 或MVC的一部份使用,那麼徹底能夠採用基於默認的FORM身份驗證受權特性(Authorize),或採用web.config中配置,這個很簡單,就不做說明了,你們能夠網上參考關於ASP.NET 或ASP.NET MVC的FORM身份驗證。
第二種:集成WINDOWS驗證
首先在WEB.CONFIG文件中,增長以下配置,以開啓WINDOWS身份驗證,配置以下:
<authentication mode="Windows"> </authentication>
而後在須要認證受權後才能訪問的Controller中類或ACTION方法上添加Authorize特性,Controller與上文相同再也不貼出,固然也能夠在WEB.CONFIG中配置:
<authorization> <deny users="?"/> </authorization>
最後將WEB API寄宿到(或者說發佈到)IIS,且須要在IIS中啓用WINDOWS身份驗證,以下圖示:
這樣就完成了該身份驗證模式(理論上WEB服務、WCF若都以IIS爲宿主,均可以採用集成WINDOWS身份驗證模式),測試方法很簡單,第一種直接在瀏覽器中訪問,第二種採用HttpClient來調用WEB API,示例代碼以下:
public async static void TestLoginApi2() { HttpClientHandler handler = new HttpClientHandler(); handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.Credentials = new NetworkCredential("admin", "www.zuowenjun.cn"); HttpClient client = new HttpClient(handler); var response = await client.GetAsync("http://localhost:8010/api/test/"); var r2 = await response.Content.ReadAsAsync<IEnumerable<string>>(); foreach (string item in r2) { Console.WriteLine("GetValues - Item Value:{0}", item); } response = await client.GetAsync("http://localhost:8010/api/test/8"); var r3 = await response.Content.ReadAsAsync<string>(); Console.WriteLine("GetValue - Item Value:{0}", r3); }
第三種:Basic基礎認證
1.定義一個繼承自AuthorizationFilterAttribute的HttpBasicAuthenticationFilter類,用於實現Basic基礎認證,實現代碼以下:
using System; using System.Net; using System.Text; using System.Web; using System.Web.Http.Controllers; using System.Web.Http.Filters; using System.Net.Http; using System.Web.Http; using System.Security.Principal; using System.Threading; using System.Net.Http.Headers; namespace WebApplication1.Models { public class HttpBasicAuthenticationFilter : AuthorizationFilterAttribute { public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext) { if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0) { base.OnAuthorization(actionContext); return; } if (Thread.CurrentPrincipal != null && Thread.CurrentPrincipal.Identity.IsAuthenticated) { base.OnAuthorization(actionContext); return; } string authParameter = null; var authValue = actionContext.Request.Headers.Authorization; if (authValue != null && authValue.Scheme == "Basic") { authParameter = authValue.Parameter; //authparameter:獲取請求中通過Base64編碼的(用戶:密碼) } if (string.IsNullOrEmpty(authParameter)) { Challenge(actionContext); return; } authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); var authToken = authParameter.Split(':'); if (authToken.Length < 2) { Challenge(actionContext); return; } if (!ValidateUser(authToken[0], authToken[1])) { Challenge(actionContext); return; } var principal = new GenericPrincipal(new GenericIdentity(authToken[0]), null); Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } base.OnAuthorization(actionContext); } private 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));//可使用以下語句 actionContext.Response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic", string.Format("realm=\"{0}\"", host))); } protected virtual bool ValidateUser(string userName, string password) { if (userName.Equals("admin", StringComparison.OrdinalIgnoreCase) && password.Equals("api.admin")) //判斷用戶名及密碼,實際可從數據庫查詢驗證,可重寫 { return true; } return false; } } }
2.在須要認證受權後才能訪問的Controller中類或ACTION方法上添加上述定義的類HttpBasicAuthenticationFilter,也可在global文件中將該類添加到全局過濾器中,便可
測試方法很簡單,第一種直接在瀏覽器中訪問(同上),第二種採用HttpClient來調用WEB API,示例代碼以下:
public async static void TestLoginApi3() { HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Authorization = CreateBasicHeader("admin", "api.admin"); var response = await client.GetAsync("http://localhost:11099/api/test/"); var r2 = await response.Content.ReadAsAsync<IEnumerable<string>>(); foreach (string item in r2) { Console.WriteLine("GetValues - Item Value:{0}", item); } response = await client.GetAsync("http://localhost:11099/api/test/8"); var r3 = await response.Content.ReadAsAsync<string>(); Console.WriteLine("GetValue - Item Value:{0}", r3); } public static AuthenticationHeaderValue CreateBasicHeader(string username, string password) { return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", username, password)))); }
實現Basic基礎認證,除了經過繼承自AuthorizationFilterAttribute來實現自定義的驗證受權過濾器外,還能夠經過繼承自DelegatingHandler來實現自定義的消息處理管道類,具體的實現方式可參見園子裏的這篇文章:
http://www.cnblogs.com/CreateMyself/p/4857799.html
第四種:Digest摘要認證
1.定義一個繼承自DelegatingHandler的HttpDigestAuthenticationHandler類,用於實如今消息管道中實現Digest摘要認證,同時定義該類所需關聯或依賴的其它類,源代碼以下:
using System; using System.Collections.Concurrent; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; namespace WebApplication1.Models { public class HttpDigestAuthenticationHandler : DelegatingHandler { protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { try { HttpRequestHeaders headers = request.Headers; if (headers.Authorization != null) { Header header = new Header(request.Headers.Authorization.Parameter, request.Method.Method); if (Nonce.IsValid(header.Nonce, header.NounceCounter)) { string password = "www.zuowenjun.cn";//默認值 //根據用戶名獲取正確的密碼,實際狀況應該從數據庫查詢 if (header.UserName.Equals("admin", StringComparison.OrdinalIgnoreCase)) { password = "api.admin";//這裏模擬獲取到的正確的密碼 } #region 計算正確的可受權的Hash值 string ha1 = String.Format("{0}:{1}:{2}", header.UserName, header.Realm, password).ToMD5Hash(); string ha2 = String.Format("{0}:{1}", header.Method, header.Uri).ToMD5Hash(); string computedResponse = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", ha1, header.Nonce, header.NounceCounter, header.Cnonce, "auth", ha2).ToMD5Hash(); #endregion if (String.CompareOrdinal(header.Response, computedResponse) == 0) //比較請求的Hash值與正確的可受權的Hash值是否相同,相則則表示驗證經過,不然失敗 { // digest computed matches the value sent by client in the response field. // Looks like an authentic client! Create a principal. // var claims = new List<Claim> //{ // new Claim(ClaimTypes.Name, header.UserName), // new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password) //}; // ClaimsPrincipal principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") }); // Thread.CurrentPrincipal = principal; // if (HttpContext.Current != null) // HttpContext.Current.User = principal; var principal = new GenericPrincipal(new GenericIdentity(header.UserName), null); Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } } } } HttpResponseMessage response = await base.SendAsync(request, cancellationToken); if (response.StatusCode == HttpStatusCode.Unauthorized) { response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest", Header.GetUnauthorizedResponseHeader(request).ToString())); } return response; } catch (Exception) { var response = request.CreateResponse(HttpStatusCode.Unauthorized); response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest", Header.GetUnauthorizedResponseHeader(request).ToString())); return response; } } } public class Header { public Header() { } public Header(string header, string method) { string keyValuePairs = header.Replace("\"", String.Empty); foreach (string keyValuePair in keyValuePairs.Split(',')) { int index = keyValuePair.IndexOf("=", System.StringComparison.Ordinal); string key = keyValuePair.Substring(0, index).Trim(); string value = keyValuePair.Substring(index + 1).Trim(); switch (key) { case "username": this.UserName = value; break; case "realm": this.Realm = value; break; case "nonce": this.Nonce = value; break; case "uri": this.Uri = value; break; case "nc": this.NounceCounter = value; break; case "cnonce": this.Cnonce = value; break; case "response": this.Response = value; break; case "method": this.Method = value; break; } } if (String.IsNullOrEmpty(this.Method)) this.Method = method; } public string Cnonce { get; private set; } public string Nonce { get; private set; } public string Realm { get; private set; } public string UserName { get; private set; } public string Uri { get; private set; } public string Response { get; private set; } public string Method { get; private set; } public string NounceCounter { get; private set; } // This property is used by the handler to generate a // nonce and get it ready to be packaged in the // WWW-Authenticate header, as part of 401 response public static Header GetUnauthorizedResponseHeader(HttpRequestMessage request) { var host = request.RequestUri.DnsSafeHost; return new Header() { Realm = host, Nonce = WebApplication1.Models.Nonce.Generate() }; } public override string ToString() { StringBuilder header = new StringBuilder(); header.AppendFormat("realm=\"{0}\"", Realm); header.AppendFormat(",nonce=\"{0}\"", Nonce); header.AppendFormat(",qop=\"{0}\"", "auth"); return header.ToString(); } } public class Nonce { private static ConcurrentDictionary<string, Tuple<int, DateTime>> nonces = new ConcurrentDictionary<string, Tuple<int, DateTime>>(); public static string Generate() { byte[] bytes = new byte[16]; using (var rngProvider = new RNGCryptoServiceProvider()) { rngProvider.GetBytes(bytes); } string nonce = bytes.ToMD5Hash(); nonces.TryAdd(nonce, new Tuple<int, DateTime>(0, DateTime.Now.AddMinutes(10))); return nonce; } public static bool IsValid(string nonce, string nonceCount) { Tuple<int, DateTime> cachedNonce = null; //nonces.TryGetValue(nonce, out cachedNonce); nonces.TryRemove(nonce, out cachedNonce);//每一個nonce只容許使用一次 if (cachedNonce != null) // nonce is found { // nonce count is greater than the one in record if (Int32.Parse(nonceCount) > cachedNonce.Item1) { // nonce has not expired yet if (cachedNonce.Item2 > DateTime.Now) { // update the dictionary to reflect the nonce count just received in this request //nonces[nonce] = new Tuple<int, DateTime>(Int32.Parse(nonceCount), cachedNonce.Item2); // Every thing looks ok - server nonce is fresh and nonce count seems to be // incremented. Does not look like replay. return true; } } } return false; } } }
using System.Linq; using System.Security.Cryptography; using System.Text; namespace WebApplication1.Models { public static class HashHelper { public static string ToMD5Hash(this byte[] bytes) { StringBuilder hash = new StringBuilder(); MD5 md5 = MD5.Create(); md5.ComputeHash(bytes) .ToList() .ForEach(b => hash.AppendFormat("{0:x2}", b)); return hash.ToString(); } public static string ToMD5Hash(this string inputString) { return Encoding.UTF8.GetBytes(inputString).ToMD5Hash(); } } }
2.將上述自定義的HttpDigestAuthenticationHandler類添加到全局消息處理管道中,代碼以下:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.MessageHandlers.Add(new HttpDigestAuthenticationHandler());//添加到消息處理管道中 } }
3.在須要認證受權後才能訪問的Controller中類或ACTION方法上添加Authorize特性便可。
測試方法很簡單,第一種直接在瀏覽器中訪問(同上),第二種採用HttpClient來調用WEB API,示例代碼以下:
public async static void TestLoginApi4() { HttpClientHandler handler = new HttpClientHandler(); handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.Credentials = new NetworkCredential("admin", "api.admin"); HttpClient client = new HttpClient(handler); var response = await client.GetAsync("http://localhost:11099/api/test/"); var r2 = await response.Content.ReadAsAsync<IEnumerable<string>>(); foreach (string item in r2) { Console.WriteLine("GetValues - Item Value:{0}", item); } response = await client.GetAsync("http://localhost:11099/api/test/8"); var r3 = await response.Content.ReadAsAsync<string>(); Console.WriteLine("GetValue - Item Value:{0}", r3); }
該實現方法,參考了該篇文章:http://zrj-software.iteye.com/blog/2163487
實現Digest摘要認證,除了上述經過繼承自DelegatingHandler來實現自定義的消息處理管道類外,也能夠經過繼承自AuthorizationFilterAttribute來實現自定義的驗證受權過濾器,Basic基礎認證與Digest摘要認證流程基本相同,區別在於:Basic是將密碼直接base64編碼(明文),而Digest是用MD5進行加密後傳輸,因此二者實現認證方式上,也基本相同。
最後說明一下,WEB SERVICE、WCF、WEB API實現身份驗證的方法有不少,每種方法都有他所適用的場景,我這個系列文章僅是列舉一些常見的實見身份驗證的方法,一是給本身複習並備忘,二是給你們以參考,文中可能有不足之處,若發現問題,能夠在下面評論指出,謝謝!