WebApi OAuth2身份认证
一、什么是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(同源策略)支持。
所有的代码都同步更新在 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/
来源:https://blog.csdn.net/qq285679784/article/details/80108389
WebApi OAuth2身份认证的更多相关文章
- C#进阶系列——WebApi 身份认证解决方案:Basic基础认证
前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...
- WebApi身份认证解决方案:Basic基础认证
前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...
- .Net WebApi 实现OAuth2.0认证
现在多数公众平台提供的api都使用OAuth2.0认证模式,最近在搞Android方面的开发,身份认证和权限控制的各方面比较来说,使用OAuth认证的还是比较靠谱,OAuth2.0的协议可以参考htt ...
- C#进阶系列——WebApi身份认证解决方案:Basic基础认证 (转)
http://www.cnblogs.com/landeanfen/p/5287064.html 前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人 ...
- .NetCore WebApi——基于JWT的简单身份认证与授权(Swagger)
上接:.NetCore WebApi——Swagger简单配置 任何项目都有权限这一关键部分.比如我们有许多接口.有的接口允许任何人访问,另有一些接口需要认证身份之后才可以访问:以保证重要数据不会泄露 ...
- WebApi 身份认证解决方案:Basic基础认证
前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...
- c# WebApi之身份验证:Basic基础认证
为什么需要身份认证 身份认证是为了提高接口访问的安全性,如果没有身份验证,那么任何匿名用户只要知道服务器的url,就可以随意访问服务器,从而访问或者操作数据库,这会是很恐怖的事. 什么是Basic基础 ...
- (转)C# WebApi 身份认证解决方案:Basic基础认证
原文地址:http://www.cnblogs.com/landeanfen/p/5287064.html 阅读目录 一.为什么需要身份认证 二.Basic基础认证的原理解析 1.常见的认证方式 2. ...
- WebApi 通过身份票据进行认证授权的具体实现
写在前面: 如果webapi接口没有身份认证,那么所有知道接口url的用户都可以随意访问接口,从而查询或者修改数据库, 那么问题就来了,如果我们不想让所有人都调用我们的接口,那么就需要加上一层验证,只 ...
随机推荐
- linux tcpdump抓包Post请求
tcpdump -s 0 -A 'tcp dst port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)' -w f ...
- js 排他思想案例
<!-- 排他思想 --> <button>按钮1</button> <button>按钮2</button> <button> ...
- Java查表法实现十进制转化成其它进制
首先了解十进制转化成二级制的原理 156的二进制为: 156 % 2 = 78 …… 0 83 % 2 = 39 …… 0 39 % 2 = 19 …… 1 19 % 2 = 9 …… 1 9 % 2 ...
- C#设计模式之12-代理模式
代理模式(Proxy Pattern) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/411 访问. 代理模式属于结构型 ...
- C#LeetCode刷题之#189-旋转数组(Rotate Array)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3700 访问. 给定一个数组,将数组中的元素向右移动 k 个位置, ...
- IDEA 非常重要的一些设置项 → 一连串的问题差点让我重新用回 Eclipse !
开心一刻 建筑行业内,我看过的最凶残笑话(IT行业内好一致!) 上联:一天晚上两个甲方三更半夜四处催图只好周五加班到周六早上七点画好八点传完九点上床睡觉十分痛苦 下联:十点才过九分甲方八个短信七个电话 ...
- asp.netcore3.1 将服务器配置为需要证书
运行 asp.netcore 3.1应用程序时,弹出证书选择框. 将服务器配置为需要证书(Kestrel),在Program.cs中,按如下所示配置 Kestrel: public static vo ...
- 初识ABP vNext(2):ABP启动模板
目录 前言 开始 AbpHelper 模块安装 最后 前言 上一篇介绍了ABP的一些基础知识,本篇继续介绍ABP的启动模板.使用ABP CLI命令就可以得到这个启动模板,其中包含了一些基础功能模块,你 ...
- go语言gRPC系列(三) - 使用grpc-gateway同时提供HTTP和gRPC服务
1. gRPC提供HTTP服务 1.1 存在的意义 1.2 代码示例 1.3 使用postman尝试调用 1.4 gRPC客户端代码调用 2. 使用grpc-gateway同时提供HTTP和gRPC服 ...
- linux上传下载小工具lrzsz
工具压缩包链接 密码:zbef 1.将压缩包放到linux的任意目录下,执行:tar zxvf lrzsz-0.12.20.tar.gz 解压压缩包 2.cd lrzsz-0.12.20 3../co ...