博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
owin Oauth
阅读量:5293 次
发布时间:2019-06-14

本文共 14142 字,大约阅读时间需要 47 分钟。

原文:http://www.cnblogs.com/richieyang/p/4918819.html

一、什么是OAuth

是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版。注意是Authorization(授权),而不是Authentication(认证)。用来做Authentication(认证)的标准叫做,我们将在以后的文章中进行介绍。

二、名词定义

理解OAuth中的专业术语能够帮助你理解其流程模式,OAuth中常用的名词术语有4个,为了便于理解这些术语,我们先假设一个很常见的授权场景:

你访问了一个日志网站(third party application),你(client)觉得这个网站很不错,准备以后就要在这个网站上写日志了,所以你准备把QQ空间(Resource owner)里面的日志都导入进来。此日志网站想要导入你在QQ空间中的日志需要知道你的QQ用户名和密码才行,为了安全期间你不会把你的QQ用户名和密码直接输入在日志网站中,所以日志网站帮你导航到了QQ认证界面(Authorization Server),当你输入完用户名和密码后,QQ认证服务器返回给日志网站一个token, 该日志网站凭借此token来访问你在QQ空间中的日志。

  1. third party application 第三方的应用,想要的到Resource owner的授权
  2. client 代表用户
  3. Resource owner 资源拥有者,在这里代表QQ
  4. Authorization server 认证服务,这里代表QQ认证服务,Resource owner和Authorization server可以是不同的服务器,也可以是同一个服务器。

三、OAuth2.0中的四种模式

OAuth定义了四种模式,覆盖了所有的授权应用场景:

  1. 授权码模式(authorization code)
  2. 简化模式(implicit)
  3. 密码模式(resource owner password credentials)
  4. 客户端模式(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)的流程:

这种模式的流程非常简单:

  1. 用户向客户端(third party application)提供用户名和密码。
  2. 客户端将用户名和密码发给认证服务器(Authorization server),向后者请求令牌(token)。
  3. 认证服务器确认无误后,向客户端提供访问令牌。
  4. 客户端持令牌(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无疑是最简单最方便的方案。

  1. 新建webApi项目
  2. 安装Nuget package:

    Microsoft.AspNet.WebApi.Owin

    Microsoft.Owin.Host.SystemWeb

  3. 增加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请求的地址,即:端口号/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发送注册用户的请求()服务器返回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是最简单的解决方案,另外一个方案是使用,这个方案的实现稍显复杂,可用的文档也较少,源码中带有几个例子我也没有直接跑起来,最后无奈之下几乎读完了整个源码才理解。

八、客户端的实现

我们将采用jquery和angular两种js框架来调用本文实现的服务端。下一篇将实现此功能,另外还要给我们的服务端加上CORS(同源策略)支持。

所有的代码都同步更新在 

参考:

作者:
来源:
声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

转载于:https://www.cnblogs.com/chenyishi/p/9565088.html

你可能感兴趣的文章
每日一库:Modernizr.js,es5-shim.js,es5-safe.js
查看>>
目录相关的操作
查看>>
解决虚拟机vmware安装64位系统“此主机支持 Intel VT-x,但 Intel VT-x 处于禁用状态”的问题...
查看>>
C++----练习--引用头文件
查看>>
11.基本包装类型
查看>>
ajax连接服务器框架
查看>>
wpf样式绑定 行为绑定 事件关联 路由事件实例
查看>>
利用maven管理项目之POM文件配置
查看>>
用HttpCombiner来减少js和css的请问次数
查看>>
FUSE-用户空间文件系统
查看>>
将tiff文件转化为jpg文件并保存
查看>>
ubuntu 16.04 开机脚本
查看>>
 VS2012 C#调用C++ dll
查看>>
TCL:表格(xls)中写入数据
查看>>
SQL SERVER 2005中如何获取日期(一个月的最后一日、一年的第一日等等)
查看>>
django 学习笔记(转)
查看>>
控制台程序秒变Windows服务(Topshelf)
查看>>
字节流与字符流的区别详解
查看>>
20141026--娱乐-箱子
查看>>
自定义分页
查看>>