Owin password
一、什么是OAuth
OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版。注意是Authorization(授权),而不是Authentication(认证)。用来做Authentication(认证)的标准叫做openid connect,我们将在以后的文章中进行介绍。
二、名词定义
理解OAuth中的专业术语能够帮助你理解其流程模式,OAuth中常用的名词术语有4个,为了便于理解这些术语,我们先假设一个很常见的授权场景:
你访问了一个日志网站(third party application),你(client)觉得这个网站很不错,准备以后就要在这个网站上写日志了,所以你准备把QQ空间(Resource owner)里面的日志都导入进来。此日志网站想要导入你在QQ空间中的日志需要知道你的QQ用户名和密码才行,为了安全期间你不会把你的QQ用户名和密码直接输入在日志网站中,所以日志网站帮你导航到了QQ认证界面(Authorization Server),当你输入完用户名和密码后,QQ认证服务器返回给日志网站一个token, 该日志网站凭借此token来访问你在QQ空间中的日志。
- third party application 第三方的应用,想要的到Resource owner的授权
 - client 代表用户
 - Resource owner 资源拥有者,在这里代表QQ
 - Authorization server 认证服务,这里代表QQ认证服务,Resource owner和Authorization server可以是不同的服务器,也可以是同一个服务器。
 
三、OAuth2.0中的四种模式
OAuth定义了四种模式,覆盖了所有的授权应用场景:
- 授权码模式(authorization code)
 - 简化模式(implicit)
 - 密码模式(resource owner password credentials)
 - 客户端模式(client credentials)
 
前面我们假设的场景可以用前两种模式来实现,不同之处在于:
当日志网站(third party application)有服务端,使用模式1;
当日志网站(third party application)没有服务端,例如纯的js+html页面需要采用模式2;
本文主描述利用OAuth2.0实现自己的WebApi认证服务,前两种模式使用场景不符合我们的需求。
四、选择合适的OAuth模式打造自己的webApi认证服务
场景:你自己实现了一套webApi,想供自己的客户端调用,又想做认证。
这种场景下你应该选择模式3或者4,特别是当你的的客户端是js+html应该选择3,当你的客户端是移动端(ios应用之类)可以选择3,也可以选择4。
密码模式(resource owner password credentials)的流程:

这种模式的流程非常简单:
- 用户向客户端(third party application)提供用户名和密码。
 - 客户端将用户名和密码发给认证服务器(Authorization server),向后者请求令牌(token)。
 - 认证服务器确认无误后,向客户端提供访问令牌。
 - 客户端持令牌(token)访问资源。
 
此时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足够信任,所以我们才能采取这种模式来实现。
五、使用owin来实现密码模式
owin集成了OAuth2.0的实现,所以在webapi中使用owin来打造authorization无疑是最简单最方便的方案。
- 新建webApi项目
 - 安装Nuget package:
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
 - 增加owin的入口类:Startup.cs
 
在项目中新建一个类,命名为Startup.cs,这个类将作为owin的启动入口,添加下面的代码
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
 | 
[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)方法:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
 | 
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类:
| 
 1 
2 
3 
4 
5 
6 
7 
 | 
public class AuthContext : IdentityDbContext<IdentityUser>    {        public AuthContext():base("AuthContext")        {                     }    } | 
同时在web.config中添加connectionString:
| 
 1 
2 
3 
 | 
<connectionStrings>  <add name="AuthContext" connectionString="Data Source=.;Initial Catalog=OAuthPractice;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /></connectionStrings> | 
5.增加一个Entities文件夹并添加UserModel类:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
 | 
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类,增加用户注册和查找功能:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
 | 
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();    }} | 
7、增加AccountController
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
 | 
[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类:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
 | 
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类:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
 | 
[RoutePrefix("api/Orders")]public class OrdersController : ApiController{    [Authorize]    [Route("")]    public List<Order> Get()    {        return Order.CreateOrders();    }} | 
我们在Get()方法上加了Authorize标签,所以此api在没有授权的情况下将返回401 Unauthorize。使用postman发个请求试试:

9. 增加OAuth认证
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
 | 
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的代码如下:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
 | 
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中。
10、注册用户
使用postman发送注册用户的请求(http://{url}/api/account/register)服务器返回200,说明注册成功。

11、向服务器请求token
resource owner password credentials模式需要body包含3个参数:
grant_type-必须为password
username-用户名
password-用户密码

12、使用token访问受保护的api
在Header中加入:Authorization – bearer {{token}},此token就是上一步得到的token。
此时客户端在30分钟内使用该token即可访问受保护的资源。30分钟这个设置来自AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),你可以自定义token过期时间。

六、刷新token
当token过期后,OAuth2.0提供了token刷新机制:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
 | 
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());} | 
1、添加新的RefreshTokenProvider
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
 | 
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。
2、重新请求token

可以看到这次请求不但得到了token,还得到了refresh_token
3、当token过期后,凭借上次得到的refresh_token重新获取token

此次请求又得到了新的refresh_token,每次refresh_token只能用一次,因为在方法ReceiveAsync中我们一旦拿到refresh_token就删除了记录。
七、总结
此文重点介绍了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,这个方案的实现稍显复杂,可用的文档也较少,源码中带有几个例子我也没有直接跑起来,最后无奈之下几乎读完了整个源码才理解。
八、客户端的实现
我们将采用jquery和angular两种js框架来调用本文实现的服务端。下一篇将实现此功能,另外还要给我们的服务端加上CORS(同源策略)支持。
http://www.cnblogs.com/richieyang/p/4918819.html
http://www.cnblogs.com/richieyang/p/5002526.html
Owin password的更多相关文章
- 基于OWIN WebAPI 使用OAuth授权服务【客户端验证授权(Resource Owner Password Credentials Grant)】
		
适用范围 前面介绍了Client Credentials Grant ,只适合客户端的模式来使用,不涉及用户相关.而Resource Owner Password Credentials Grant模 ...
 - asp.net权限认证:OWIN实现OAuth 2.0 之密码模式(Resource Owner Password Credential)
		
asp.net权限认证系列 asp.net权限认证:Forms认证 asp.net权限认证:HTTP基本认证(http basic) asp.net权限认证:Windows认证 asp.net权限认证 ...
 - ASP.NET WebApi OWIN 实现 OAuth 2.0
		
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用. OAuth 允许用户提供一个令牌, ...
 - MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN
		
在Membership系列的最后一篇引入了ASP.NET Identity,看到大家对它还是挺感兴趣的,于是来一篇详解登录原理的文章.本文会涉及到Claims-based(基于声明)的认证,我们会详细 ...
 - ASP.NET OWIN OAuth:遇到的2个refresh token问题
		
之前写过2篇关于refresh token的生成与持久化的博文:1)Web API与OAuth:既生access token,何生refresh token:2)ASP.NET OWIN OAuth: ...
 - ASP.NET OWIN OAuth:refresh token的持久化
		
在前一篇博文中,我们初步地了解了refresh token的用途——它是用于刷新access token的一种token,并且用简单的示例代码体验了一下获取refresh token并且用它刷新acc ...
 - ASP.NET Web API与Owin OAuth:调用与用户相关的Web API
		
在前一篇博文中,我们通过以 OAuth 的 Client Credential Grant 授权方式(只验证调用客户端,不验证登录用户)拿到的 Access Token ,成功调用了与用户无关的 We ...
 - SimpleSSO:使用Microsoft.Owin.Security.OAuth搭建OAuth2.0授权服务端
		
目录 前言 OAuth2.0简介 授权模式 (SimpleSSO示例) 使用Microsoft.Owin.Security.SimpleSSO模拟OpenID认证 通过authorization co ...
 - [转]MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN
		
本文转自:http://www.cnblogs.com/jesse2013/p/aspnet-identity-claims-based-authentication-and-owin.html 在M ...
 
随机推荐
- Mybatis相关SQL操作总结
			
1.resultMap和resultType等参数以及结果集 <select id="getApplicationByRoleCode" resultType="p ...
 - Java程序员如何在竞争中保持优势
			
Java程序员入门容易,进阶很难,想要在竞争中保持优势,脚踏实地的同时也要仰望星空规划自己的未来.时间在流逝,年龄在增加,你期望的薪水也在不断增多,你总得让自己能力持续增加以配得上想要的收入吧. 从初 ...
 - 虚拟环境中的django及相关包安装
			
1.先进入虚拟环境 pyenv activate virtualenvname. 2.安装django软件包 安装命令: pip install django==1.7 查看安装结果: python ...
 - jenkins jacoco
			
1.pom中加jacoco依赖 <dependency> <groupId>org.jacoco</groupId> <artifactId>jacoc ...
 - mysql 自增id
			
在开发的时候遇到了 自增id变成2147483647 莫名其妙 然后发现是自己没把自增id改为 无符号的原因 把无符号勾上就ok了
 - 【转】 Pro Android学习笔记(八九):了解Handler(3):延迟执行小例子
			
目录(?)[-] 小例子 Handler的处理 Activity的代码片段 后台线程和UI的互动 文章转载只能用于非商业性质,且不能带有虚拟货币.积分.注册等附加条件.转载须注明出处:http://b ...
 - maven如何过滤占位符
			
今天遇到一个问题,就是properties文件中赋值用的这种形式${xxx},真正的值是配置在pom的profile中,但是未生效. 后来找到原因,原来是pom中少了一段代码: <build&g ...
 - CKEditor 自定义按钮插入服务端图片
			
CKEditor 富文本编辑器很好用,功能很强大,在加上支持服务端图片上传的CKFinder更是方便, 最近在使用CKFinder的时候发现存在很多问题,比如上传图片的时候,图片不能按时间降序排列,另 ...
 - 关于多账套API的设计
			
帐套在财务核算中是记载一个独立核算的经济实体的所有往来信息的一整套记录表和统计分析报表.不同的帐套之间的关系是相对独立的,对其中的任何一个帐套中的数据进行建立.删除或修改都不会影响其他帐套.在ERP中 ...
 - 10-14C#基础--语句(switch....case和for...循环)
			
10-14C#基础--语句(2) 一.课前作业:“跟电脑猜拳” 二.switch(定义的变量,参数值)......case.... 注:switch...case大多用于值类型的判断,这里不同于if表 ...