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