http://www.cnblogs.com/dudu/p/4569857.htmlhtml
OAuth2.0ios
1、什麼是OAuthgit
OAuth是一個關於受權(Authorization)的開放網絡標準,目前的版本是2.2版。github
注意是Authorization(受權),而不是Authentication(認證)。web
用來作Authentication(認證)的標準叫作openid connect。json
2、名詞定義api
理解OAuth中的專業術語可以幫助你理解其流程模式,OAuth中經常使用的名詞術語有4個,爲了便於理解這些術語,咱們先假設一個很常見的受權場景:安全
你訪問了一個日誌網站(third party application),你(client)以爲這個網站很不錯,準備之後就要在這個網站上寫日誌了,因此你準備把QQ空間(Resource owner)裏面的日誌都導入進來。此日誌網站想要導入你在QQ空間中的日誌須要知道你的QQ用戶名和密碼才行,爲了安全期間你不會把你的QQ用戶名和密碼直接輸入在日誌網站中,因此日誌網站幫你導航到了QQ認證界面(Authorization Server),當你輸入完用戶名和密碼後,QQ認證服務器返回給日誌網站一個token, 該日誌網站憑藉此token來訪問你在QQ空間中的日誌。服務器
3、OAuth2.0 中的四種模式網絡
OAuth定義了四種模式,覆蓋了全部的受權應用場景:
前面咱們假設的場景能夠用前兩種模式來實現,不一樣之處在於:
當日志網站(third party application)有服務端,使用模式1;
當日志網站(third party application)沒有服務端,例如純的js+html頁面須要採用模式2;
本文主描述利用OAuth2.0實現本身的WebApi認證服務,前兩種模式使用場景不符合咱們的需求。
4、選擇合適的OAuth模式打造本身的webApi認證服務
場景:你本身實現了一套webApi,想供本身的客戶端調用,又想作認證。
這種場景下你應該選擇模式3或者4,特別是當你的的客戶端是js+html應該選擇3,當你的客戶端是移動端(ios應用之類)能夠選擇3,也能夠選擇4。
密碼模式(resource owner password credentials)的流程:
這種模式的流程很是簡單:
此時third party application表明咱們本身的客戶端,Authorization server和Resource owner表明咱們本身的webApi服務。咱們在日誌網站的場景中提到:用戶不能直接爲日誌網站(third party application)提供QQ(resource owner)的用戶名和密碼。而此時third party application、authorization server、resource owner都是一家人,Resource owner對third party application足夠信任,因此咱們才能採起這種模式來實現。
5、使用owin來實現密碼模式
owin集成了OAuth2.0的實現,因此在webapi中使用owin來打造authorization無疑是最簡單最方便的方案。
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
在項目中新建一個類,命名爲Startup.cs,這個類將做爲owin的啓動入口,添加下面的代碼
using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Infrastructure; using Microsoft.Owin.Security.OAuth; using Owin; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Http; using ADT.TuDou.API.Auth; [assembly: OwinStartup(typeof(ADT.TuDou.API.Startup))] namespace ADT.TuDou.API { public class Startup { //ConfigureOAuth(IAppBuilder app)方法開啓了OAuth服務。簡單說一下OAuthAuthorizationServerOptions中各參數的含義: //AllowInsecureHttp:容許客戶端使用http協議請求; //TokenEndpointPath:token請求的地址,即http://localhost:端口號/token; //AccessTokenExpireTimeSpan :token過時時間; //註冊用戶 http://localhost:33590/api/account/register) //content-type: application/json //{"UserName":"jay","Password":"xsj1989","ConfirmPassword":"xsj1989"} //獲取token方法 //獲取token的地址:http://localhost:33590/token //提交方式:POST //參數:grant_type=password&username=jay&password=xsj1989 //內容類型:content-type: application/x-www-form-urlencoded //返回數據:{"access_token":"JznxS2sYbU9fItS-ihnHB6kPnqzFd-C8uZGargqv2TF6mrNhvZYLY4OG1VclTh2PYkqLgeGzZQrnicf633coKEiJsHQCVZQTMHwli1uNQ3fJF2t0ab3CIO7Kj8y2ZvCS5ypOLAOuKkpkP1oAgCJHMkVOMZRbPfj1tqijPSt1EKcRPfzZMcOo0-OxYmbqjBrkHoB-18ZApYy4kyG6g7cHX-kh3Fq4TEAFeShfk5lOn7NKJxUJf9RWs---tCWwcqWVI-XwA3am0G8KW95-OEDq6d1gr2qHxeK020bhbvQ-OWSiR8MEq617wi-jWqdngdl_","token_type":"bearer","expires_in":1799,"as:client_id":"","userName":"jay",".issued":"Thu, 17 Mar 2016 06:19:04 GMT",".expires":"Thu, 17 Mar 2016 06:49:04 GMT"} //使用token訪問受保護的接口 //http://localhost:33590/api/orders //User-Agent: Fiddler //Host: localhost:33590 //Content-Length: 51 //content-type: application/x-www-form-urlencoded //Authorization: Bearer UcaIhVTpveBVlAVEhwp1iELcd2jMhHLzRdKKcTmja1Ii5520PQ6fr56kt1mN_7O92WBrkg0AkR45i1BvPmiuIAtVCk-aJsvKd7w0uaBJIGnQBycjR3WyW-plFtlGrErtEbxTOjLe4QpLgn6ofTB61wK4MS7RR91skVEhIUt4NPY0gYKn_EqE1ihPoOMuYAIciQUmQH9aKDyo3tYFjDrhtRGS6SfSBoWFdRaIPEOtQvFG4KMnbCO1XymYAsDS0vDnUZ_BgcQAYC_PbYfNRCTGfAkwDc4hidiotwde0---nPpUt2YXbFfI8oWQ48Jgi_Fk //1.刷新Token,再次請求:http://localhost:33590/token //返回數據: //{"access_token":"vZPcwG6szzTaybqPWsS7ESlxMKZa7kYDHTwwbXzyEes8-9kwPTCUaf3a8vbq8qp99l265jwzqXMDzMmWa89kmCPHMI-82OYgZVhw86qtbYUGTvFEwHEyysmGD9MAH524CBbaDsCJSl1sg-VaBIC8wgl1pAHZTh56__iHj3ASUhTvphT68GpU6TyhVhnIuBTonkVoE7vjpNjIdzvwGFshHGrJKS84iZYTKlM9Kv7AItrDsRon-QwfgQOmCZ2ceCvUH8sGI5O2CYJVYhH8p7TRGLgBa4p0LYnbMC54xFyO8ZmjXr7vpG0LVTsyd0q-c6pd","token_type":"bearer","expires_in":1799,"refresh_token":"d5fb57fcc7084d128ad70e5e65643045","as:client_id":"","userName":"jay",".issued":"Fri, 18 Mar 2016 02:28:21 GMT",".expires":"Fri, 18 Mar 2016 02:58:21 GMT"} //包含數據:"refresh_token":"d5fb57fcc7084d128ad70e5e65643045" //當token過時後,憑藉上次獲得的refresh_token從新獲取token //2.再次請求:http://localhost:33590/token //參數:grant_type=refresh_token&refresh_token=d5fb57fcc7084d128ad70e5e65643045 //報錯:invalid_grant 暫時沒解決 //Provider :提供具體的認證策略; public void Configuration(IAppBuilder app) { var config = new HttpConfiguration(); WebApiConfig.Register(config); ConfigureOAuth(app); //這一行代碼必須放在ConfiureOAuth(app)以後 app.UseWebApi(config); } public void ConfigureOAuth(IAppBuilder app) { OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() { AllowInsecureHttp = true, TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30), Provider = new SimpleAuthorizationServerProvider(), //refresh token provider RefreshTokenProvider = new SimpleRefreshTokenProvider() }; // Token Generation app.UseOAuthAuthorizationServer(OAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); //ConfigureOAuth(IAppBuilder app)方法開啓了OAuth服務。簡單說一下OAuthAuthorizationServerOptions中各參數的含義: //AllowInsecureHttp:容許客戶端使用http協議請求; //TokenEndpointPath:token請求的地址,即http://localhost:端口號/token; //AccessTokenExpireTimeSpan :token過時時間; //Provider :提供具體的認證策略; } } public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider { public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); return Task.FromResult<object>(null); } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { using (AuthRepository _repo = new AuthRepository()) { IdentityUser user = await _repo.FindUser(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "The user name or password is incorrect."); return; } } var identity = new ClaimsIdentity(context.Options.AuthenticationType); identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); identity.AddClaim(new Claim(ClaimTypes.Role, "user")); identity.AddClaim(new Claim("sub", context.UserName)); var props = new AuthenticationProperties(new Dictionary<string, string> { { "as:client_id", context.ClientId ?? string.Empty }, { "userName", context.UserName } }); var ticket = new AuthenticationTicket(identity, props); context.Validated(ticket); } public override Task TokenEndpoint(OAuthTokenEndpointContext context) { foreach (KeyValuePair<string, string> property in context.Properties.Dictionary) { context.AdditionalResponseParameters.Add(property.Key, property.Value); } return Task.FromResult<object>(null); } } public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider { public async Task CreateAsync(AuthenticationTokenCreateContext context) { var refreshTokenId = Guid.NewGuid().ToString("n"); using (AuthRepository _repo = new AuthRepository()) { var token = new RefreshToken() { Id = refreshTokenId.GetHashCode(), Subject = context.Ticket.Identity.Name, IssuedUtc = DateTime.UtcNow, ExpiresUtc = DateTime.UtcNow.AddMinutes(30) }; context.Ticket.Properties.IssuedUtc = token.IssuedUtc; context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc; token.ProtectedTicket = context.SerializeTicket(); var result = await _repo.AddRefreshToken(token); if (result) { context.SetToken(refreshTokenId); } } } public async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { int hashedTokenId = context.Token.GetHashCode(); using (AuthRepository _repo = new AuthRepository()) { var refreshToken = await _repo.FindRefreshToken(hashedTokenId); if (refreshToken != null) { //Get protectedTicket from refreshToken class context.DeserializeTicket(refreshToken.ProtectedTicket); var result = await _repo.RemoveRefreshToken(hashedTokenId); } } } public void Create(AuthenticationTokenCreateContext context) { throw new NotImplementedException(); } public void Receive(AuthenticationTokenReceiveContext context) { throw new NotImplementedException(); } } public class RefreshToken { public int Id { get; set; } public string Subject { get; set; } public DateTime IssuedUtc { get; set; } public DateTime ExpiresUtc { get; set; } public string ProtectedTicket { get; set; } }; }
另外修改WebApiConfig.Register(HttpConfiguration config)方法:
public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); //config.Routes.MapHttpRoute( // name: "DefaultApi", // routeTemplate: "api/{controller}/{id}", // defaults: new { id = RouteParameter.Optional } //); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); //DO:將會使用CamelCase命名法序列化webApi的返回結果 var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First(); jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); }
最後兩句話將會使用CamelCase命名法序列化webApi的返回結果。
3.使用ASP.NET Identity 實現一個簡單的用戶認證功能,以便咱們生成用戶名和密碼
安裝nuget package:
Microsoft.AspNet.Identity.Owin
Microsoft.AspNet.Identity.EntityFramework
4.新建一個Auth的文件夾,並添加類:
using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Web; namespace ADT.TuDou.API.Auth { public class AuthContext : IdentityDbContext<IdentityUser> { public AuthContext() : base("AuthContext") { } public DbSet<RefreshToken> RefreshTokens { get; set; } } }
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web; using ADT.TuDou.API.Entities; namespace ADT.TuDou.API.Auth { public class AuthRepository : IDisposable { private AuthContext _ctx; private UserManager<IdentityUser> _userManager; public AuthRepository() { _ctx = new AuthContext(); _userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(_ctx)); } //註冊用戶 public async Task<IdentityResult> RegisterUser(UserModel userModel) { IdentityUser user = new IdentityUser { UserName = userModel.UserName }; var result = await _userManager.CreateAsync(user, userModel.Password); return result; } //查詢用戶 public async Task<IdentityUser> FindUser(string userName, string password) { IdentityUser user = await _userManager.FindAsync(userName, password); return user; } public async Task<bool> AddRefreshToken(RefreshToken token) { var existingToken = _ctx.RefreshTokens.SingleOrDefault(r => r.Subject == token.Subject); if (existingToken != null) { var result = await RemoveRefreshToken(existingToken); } _ctx.RefreshTokens.Add(token); return await _ctx.SaveChangesAsync() > 0; } public async Task<bool> RemoveRefreshToken(int refreshTokenId) { var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId); if (refreshToken != null) { _ctx.RefreshTokens.Remove(refreshToken); return await _ctx.SaveChangesAsync() > 0; } return false; } public async Task<bool> RemoveRefreshToken(RefreshToken refreshToken) { _ctx.RefreshTokens.Remove(refreshToken); return await _ctx.SaveChangesAsync() > 0; } public async Task<RefreshToken> FindRefreshToken(int refreshTokenId) { var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId); return refreshToken; } public void Dispose() { _ctx.Dispose(); _userManager.Dispose(); } } }
同時在web.config中添加connectionString:
<connectionStrings> <add name="AuthContext" connectionString="Data Source=.;Initial Catalog=OAuthPractice;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /> </connectionStrings>
5.增長一個Entities文件夾並添加UserModel類:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web; namespace ADT.TuDou.API.Entities { public class UserModel { [Required] [Display(Name = "UserModel name")] public string UserName { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } }
六、增長TestController:ApiController
/// <summary> /// 依據用戶ID,得到頭像 /// </summary> /// <returns></returns> [AllowAnonymous] [AcceptVerbs("GET")] public HttpResponseMessage GetHeadImg(int UserID) { //......... }
方法上加了AllowAnonymous標籤,意味着調用這個api無需任何受權
方法上加了Authorize標籤,則此api在沒有受權的狀況下將返回401 Unauthorize。
七、類 SimpleAuthorizationServerProvider 說明:
ValidateClientAuthentication方法用來對third party application 認證,具體的作法是爲third party application頒發appKey和appSecrect,在本例中咱們省略了頒發appKey和appSecrect的環節,咱們認爲全部的third party application都是合法的,context.Validated(); 表示全部容許此third party application請求。
GrantResourceOwnerCredentials方法則是resource owner password credentials模式的重點,因爲客戶端發送了用戶的用戶名和密碼,因此咱們在這裏驗證用戶名和密碼是否正確,後面的代碼採用了ClaimsIdentity認證方式,其實咱們能夠把他看成一個NameValueCollection看待。最後context.Validated(ticket); 代表認證經過。
只有這兩個方法同時認證經過纔會頒發token。
TokenEndpoint方法將會把Context中的屬性加入到token中。
八、向服務器請求token
resource owner password credentials模式須要body包含3個參數:
grant_type-必須爲password
username-用戶名
password-用戶密碼
//獲取token方法
//獲取token的地址:http://localhost:33590/token
//提交方式:POST
//參數:grant_type=password&username=jay&password=xsj1989
//內容類型:content-type: application/x-www-form-urlencoded
//返回數據:{"access_token":"JznxS2sYbU9fItS-ihnHB6kPnqzFd-C8uZGargqv2TF6mrNhvZYLY4OG1VclTh2PYkqLgeGzZQrnicf633coKEiJsHQCVZQTMHwli1uNQ3fJF2t0ab3CIO7Kj8y2ZvCS5ypOLAOuKkpkP1oAgCJHMkVOMZRbPfj1tqijPSt1EKcRPfzZMcOo0-OxYmbqjBrkHoB-18ZApYy4kyG6g7cHX-kh3Fq4TEAFeShfk5lOn7NKJxUJf9RWs---tCWwcqWVI-XwA3am0G8KW95-OEDq6d1gr2qHxeK020bhbvQ-OWSiR8MEq617wi-jWqdngdl_","token_type":"bearer","expires_in":1799,"as:client_id":"","userName":"jay",".issued":"Thu, 17 Mar 2016 06:19:04 GMT",".expires":"Thu, 17 Mar 2016 06:49:04 GMT"}
實踐中的問題:
異常:安全透明方法「System.Web.Http.GlobalConfiguration.get_Configuration()」嘗試訪問安全關鍵類型「System.Web.Http.HttpConfiguration」失敗
解決:Nuget 安裝 Microsoft.AspNet.WebApi -IncludePrerelease 包,更新爲最新的包。
參考:
http://www.cnblogs.com/richieyang/p/4918819.html?utm_source=tuicool&utm_medium=referral#undefinedhttp://oauth.net/code/http://www.cnblogs.com/n-pei/archive/2012/05/29/2524673.htmlhttps://github.com/feiyit/MvcApiSecurity