关于WEB ServiceWCFWebApi实现身份验证之WebApi篇
之前先后總結并發(fā)表了關于WEB Service、WCF身份驗證相關文章,如下:
關于WEB Service&WCF&WebApi實現(xiàn)身份驗證之WEB Service篇、
關于WEB Service&WCF&WebApi實現(xiàn)身份驗證之WCF篇(1)、關于WEB Service&WCF&WebApi實現(xiàn)身份驗證之WCF篇(2)
今天再來總結關于如何實現(xiàn)WebApi的身份驗證,以完成該系列所有文章,WebApi常見的實現(xiàn)方式有:FORM身份驗證、集成WINDOWS驗證、Basic基礎認證、Digest摘要認證
?第一種: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,用于登錄入口,示例代碼如下:
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)){//創(chuàng)建票據FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, uname, DateTime.Now, DateTime.Now.AddMinutes(30), false, string.Empty);//加密票據string authTicket = FormsAuthentication.Encrypt(ticket);//存儲為cookieHttpCookie 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) });//測試用:當?shù)卿浭r,清除可能存在的身份驗證Cookiereturn Request.CreateErrorResponse(HttpStatusCode.NotFound, "登錄失敗,無效的用戶名或密碼!");}}// GET api/testpublic IEnumerable<string> GetValues(){return new string[] { "value1", "value2" };}// GET api/test/5public string GetValue(int id){return "value";}} }測試用法一:可直接在瀏覽器中訪問需要授權的方法(即:Login除外),如:http://localhost:11099/api/test/,響應結果如下:
請求頭信息如下:
若成功調用Login方法后(http://localhost:11099/api/test/login?uname=admin&pwd=api.admin),再調用上述方法,則可以獲得正常的結果,如下圖示:
看一下請求時附帶的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數(shù)量:" + 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寄宿到(或者說發(fā)布到)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類,用于實現(xiàn)Basic基礎認證,實現(xiàn)代碼如下:
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")) //判斷用戶名及密碼,實際可從數(shù)據庫查詢驗證,可重寫{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))));}實現(xiàn)Basic基礎認證,除了通過繼承自AuthorizationFilterAttribute來實現(xiàn)自定義的驗證授權過濾器外,還可以通過繼承自DelegatingHandler來實現(xiàn)自定義的消息處理管道類,具體的實現(xiàn)方式可參見園子里的這篇文章:
http://www.cnblogs.com/CreateMyself/p/4857799.html
?第四種:Digest摘要認證
?1.定義一個繼承自DelegatingHandler的HttpDigestAuthenticationHandler類,用于實現(xiàn)在消息管道中實現(xiàn)Digest摘要認證,同時定義該類所需關聯(lián)或依賴的其它類,源代碼如下:
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";//默認值//根據用戶名獲取正確的密碼,實際情況應該從數(shù)據庫查詢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();#endregionif (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 responsepublic 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 recordif (Int32.Parse(nonceCount) > cachedNonce.Item1){// nonce has not expired yetif (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);}該實現(xiàn)方法,參考了該篇文章:http://zrj-software.iteye.com/blog/2163487
實現(xiàn)Digest摘要認證,除了上述通過繼承自DelegatingHandler來實現(xiàn)自定義的消息處理管道類外,也可以通過繼承自AuthorizationFilterAttribute來實現(xiàn)自定義的驗證授權過濾器,Basic基礎認證與Digest摘要認證流程基本相同,區(qū)別在于:Basic是將密碼直接base64編碼(明文),而Digest是用MD5進行加密后傳輸,所以兩者實現(xiàn)認證方式上,也基本相同。
最后說明一下,WEB SERVICE、WCF、WEB API實現(xiàn)身份驗證的方法有很多,每種方法都有他所適用的場景,我這個系列文章僅是列舉一些常見的實見身份驗證的方法,一是給自己復習并備忘,二是給大家以參考,文中可能有不足之處,若發(fā)現(xiàn)問題,可以在下面評論指出,謝謝!
總結
以上是生活随笔為你收集整理的关于WEB ServiceWCFWebApi实现身份验证之WebApi篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VMware Workstation
- 下一篇: Nginx 负载均衡配置和策略