1、什么是OAuthhtml
OAuth是一个关于受权(Authorization)的开放网络标准,目前的版本是2.0版。注意是Authorization(受权),而不是Authentication(认证)。用来作Authentication(认证)的标准叫作openid connect,咱们将在之后的文章中进行介绍。jquery
2、名词定义ios
理解OAuth中的专业术语可以帮助你理解其流程模式,OAuth中经常使用的名词术语有4个,为了便于理解这些术语,咱们先假设一个很常见的受权场景:git
你访问了一个日志网站(third party application),你(client)以为这个网站很不错,准备之后就要在这个网站上写日志了,因此你准备把QQ空间(Resource owner)里面的日志都导入进来。此日志网站想要导入你在QQ空间中的日志须要知道你的QQ用户名和密码才行,为了安全期间你不会把你的QQ用户名和密码直接输入在日志网站中,因此日志网站帮你导航到了QQ认证界面(Authorization Server),当你输入完用户名和密码后,QQ认证服务器返回给日志网站一个token, 该日志网站凭借此token来访问你在QQ空间中的日志。web
3、OAuth2.0中的四种模式数据库
OAuth定义了四种模式,覆盖了全部的受权应用场景:json
前面咱们假设的场景能够用前两种模式来实现,不一样之处在于:api
当日志网站(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的启动入口,添加下面的代码
[assembly: OwinStartup(typeof(OAuthPractice.ProtectedApi.Startup))] namespace OAuthPractice.ProtectedApi { public class Startup { public void Configuration(IAppBuilder app) { var config = new HttpConfiguration(); WebApiConfig.Register(config); app.UseWebApi(config); } } }
另外修改WebApiConfig.Register(HttpConfiguration config)方法:
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 } ); 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的文件夹,并添加AuthContext类:
public class AuthContext : IdentityDbContext<IdentityUser> { public AuthContext():base("AuthContext") { } }
同时在web.config中添加connectionString:
<connectionStrings> <add name="AuthContext" connectionString="Data Source=.;Initial Catalog=OAuthPractice;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /> </connectionStrings>
5.增长一个Entities文件夹并添加UserModel类:
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; } }
6.在Auth文件夹下添加AuthRepository类,增长用户注册和查找功能:
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 void Dispose() { _ctx.Dispose(); _userManager.Dispose(); } }
七、增长AccountController
[RoutePrefix("api/Account")] public class AccountController : ApiController { private readonly AuthRepository _authRepository = null; public AccountController() { _authRepository = new AuthRepository(); } // POST api/Account/Register [AllowAnonymous] [Route("Register")] public async Task<IHttpActionResult> Register(UserModel userModel) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result = await _authRepository.RegisterUser(userModel); IHttpActionResult errorResult = GetErrorResult(result); if (errorResult != null) { return errorResult; } return Ok(); } protected override void Dispose(bool disposing) { if (disposing) { _authRepository.Dispose(); } base.Dispose(disposing); } private IHttpActionResult GetErrorResult(IdentityResult result) { if (result == null) { return InternalServerError(); } if (!result.Succeeded) { if (result.Errors != null) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } } if (ModelState.IsValid) { // No ModelState errors are available to send, so just return an empty BadRequest. return BadRequest(); } return BadRequest(ModelState); } return null; } }
Register方法打上了AllowAnonymous标签,意味着调用这个api无需任何受权。
8.增长一个OrderControll,添加一个受保护的api用来作实验
在Models文件夹下增长Order类:
public class Order { public int OrderID { get; set; } public string CustomerName { get; set; } public string ShipperCity { get; set; } public Boolean IsShipped { get; set; } public static List<Order> CreateOrders() { List<Order> OrderList = new List<Order> { new Order {OrderID = 10248, CustomerName = "Taiseer Joudeh", ShipperCity = "Amman", IsShipped = true }, new Order {OrderID = 10249, CustomerName = "Ahmad Hasan", ShipperCity = "Dubai", IsShipped = false}, new Order {OrderID = 10250,CustomerName = "Tamer Yaser", ShipperCity = "Jeddah", IsShipped = false }, new Order {OrderID = 10251,CustomerName = "Lina Majed", ShipperCity = "Abu Dhabi", IsShipped = false}, new Order {OrderID = 10252,CustomerName = "Yasmeen Rami", ShipperCity = "Kuwait", IsShipped = true} }; return OrderList; } }
增长OrderController类:
[RoutePrefix("api/Orders")] public class OrdersController : ApiController { [Authorize] [Route("")] public List<Order> Get() { return Order.CreateOrders(); } }
咱们在Get()方法上加了Authorize标签,因此此api在没有受权的状况下将返回401 Unauthorize。使用postman发个请求试试:
9. 增长OAuth认证
public class Startup { 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() }; // Token Generation app.UseOAuthAuthorizationServer(OAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); }
ConfigureOAuth(IAppBuilder app)方法开启了OAuth服务。简单说一下OAuthAuthorizationServerOptions中各参数的含义:
AllowInsecureHttp:容许客户端使用http协议请求;
TokenEndpointPath:token请求的地址,即http://localhost:端口号/token;
AccessTokenExpireTimeSpan :token过时时间;
Provider :提供具体的认证策略;
SimpleAuthorizationServerProvider的代码以下:
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); } }
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中。
十、注册用户
使用postman发送注册用户的请求(http://{url}/api/account/register)服务器返回200,说明注册成功。
十一、向服务器请求token
resource owner password credentials模式须要body包含3个参数:
grant_type-必须为password
username-用户名
password-用户密码
十二、使用token访问受保护的api
在Header中加入:Authorization – bearer {{token}},此token就是上一步获得的token。
此时客户端在30分钟内使用该token便可访问受保护的资源。30分钟这个设置来自AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),你能够自定义token过时时间。
6、刷新token
当token过时后,OAuth2.0提供了token刷新机制:
public void ConfigureOAuth(IAppBuilder app) { OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() { AllowInsecureHttp = true, TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), Provider = new SimpleAuthorizationServerProvider(), //refresh token provider RefreshTokenProvider = new SimpleRefreshTokenProvider() }; // Token Generation app.UseOAuthAuthorizationServer(OAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); }
一、添加新的RefreshTokenProvider
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.GetHash(), 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) { string hashedTokenId = context.Token.GetHash(); 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(); } }
咱们实现了其中两个异步方法,对两个同步方法不作实现。其中CreateAsync用来生成RefreshToken值,生成后须要持久化在数据库中,客户端须要拿RefreshToken来请求刷新token,此时ReceiveAsync方法将拿客户的RefreshToken和数据库中RefreshToken作对比,验证成功后删除此refreshToken。
二、从新请求token
能够看到此次请求不但获得了token,还获得了refresh_token
三、当token过时后,凭借上次获得的refresh_token从新获取token
这次请求又获得了新的refresh_token,每次refresh_token只能用一次,由于在方法ReceiveAsync中咱们一旦拿到refresh_token就删除了记录。
7、总结
此文重点介绍了OAuth2.0中resource owner password credentials模式的使用,此模式能够实现资源服务为本身的客户端受权。另外文章中也提到模式4-client credentials也能够实现这种场景,但用来给有服务端的客户端使用-区别于纯html+js客户端。缘由在于模式4-client credentials使用appKey+appSecrect来验证客户端,若是没有服务端的话appSecrect将暴露在js中。
一样的道理:模式1-受权码模式(authorization code)和模式2-简化模式(implicit)的区别也在于模式2-简化模式(implicit)用在无服务端的场景下,请求头中不用带appSecrect。
在webApi中使用owin来实现OAuth2.0是最简单的解决方案,另一个方案是使用DotNetOpenOauth,这个方案的实现稍显复杂,可用的文档也较少,源码中带有几个例子我也没有直接跑起来,最后无奈之下几乎读完了整个源码才理解。
8、客户端的实现
咱们将采用jquery和angular两种js框架来调用本文实现的服务端。下一篇将实现此功能,另外还要给咱们的服务端加上CORS(同源策略)支持。
全部的代码都同步更新在 https://git.oschina.net/richieyangs/OAuthPractice.git
使用OAuth打造webapi认证服务供本身的客户端使用(二)
参考:
http://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server
http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api
http://bitoftech.net/2014/06/01/token-based-authentication-asp-net-web-api-2-owin-asp-net-identity/