使用OAuth打造webapi认证服务供自己的客户端使用
一、什么是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的启动入口,添加下面的代码
[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(); }
}
7、增加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中。
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刷新机制:
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
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/
使用OAuth打造webapi认证服务供自己的客户端使用的更多相关文章
- 使用OAuth打造webapi认证服务供自己的客户端使用(二)
在上一篇”使用OAuth打造webapi认证服务供自己的客户端使用“的文章中我们实现了一个采用了OAuth流程3-密码模式(resource owner password credentials)的W ...
- OAuth打造webapi认证服务
使用OAuth打造webapi认证服务供自己的客户端使用 一.什么是OAuth OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版.注意是Authorizati ...
- OAuth2建立webapi认证服务供自己的客户端使用--密码模式
场景:你自己实现了一套webApi,想供自己的客户端调用,又想做认证. 第一步:通过vs2015建立web api项目,Startup.cs,这个类将会作为Owin的启动类. 第二步:在webapi. ...
- OAuth在WebApi中的使用,前后台分离的调用方式
前段时间由于公司架构服务层向WebApi转换,就研究了OAuth在WebApi中的使用,这中间遇到了很多坑,在此记录一下OAuth的正确使用方式. 1. OAuth是做什么的? 在网上浏览时,大家都 ...
- OAuth在WebApi
OAuth在WebApi中的使用,前后台分离的调用方式 前段时间由于公司架构服务层向WebApi转换,就研究了OAuth在WebApi中的使用,这中间遇到了很多坑,在此记录一下OAuth的正确使用方式 ...
- OAuth做webapi认证
OAuth做webapi认证 看到园子里面有人写的OAuth,就想把自己实现的OAuth也分享一下,关于OAuth协议这里就不再赘述. 一.作为认证服务器,首先需要提供一个可以通过appid/apps ...
- 一次asp.net core3.1打造webapi开发框架的实践
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAigAAAAbCAYAAABWfHSvAAAH30lEQVR4nO1dy5GsMAx80RIESRAEST ...
- 自己开发实现OAuth做webapi认证
看到园子里面有人写的OAuth,就想把自己实现的OAuth也分享一下,关于OAuth协议这里就不再赘述. 一.作为认证服务器,首先需要提供一个可以通过appid/appsecret来获取token这样 ...
- Tomcat 配置用户认证服务供C#客户端调用
项目里,遇到的一个小问题来好好的总结一下.因为我们这个项目是用Java写的服务端发布WebService,客户端呢使用C#来调用WebService(本人以前搞过一段时间C#客户端,还总结了一个MVP ...
随机推荐
- MySQL Can't connect to MySQL server on 'localhost' (10061)
run > services.msc > rightclick MySQL > properties >start 搞定
- 纯jQuery-添加/修改/删除 标签,属性
<h1>通过学习<精彩绝伦的jQuery>与W3C,大致了解JQuery的一些方法.</h1> PS:需要有一些前置条件,比如JQuery源代码,比如html就要有 ...
- 关于Java8函数式编程你需要了解的几点
函数式编程与面向对象的设计方法在思路和手段上都各有千秋,在这里,我将简要介绍一下函数式编程与面向对象相比的一些特点和差异. 函数作为一等公民 在理解函数作为一等公民这句话时,让我们先来看一下一种非常常 ...
- 使用Gemini构建自己的IDE
你的项目中的领域特定语言是否需要自己的IDE?Visual Studio Shell是选择之一,但是过于庞大不易部署,而且很难使用.Tim Jones的Gemini框架是一个轻量级替代方案. Gemi ...
- 玩转Asp.net MVC 的八个扩展点
MVC模型以低耦合.可重用.可维护性高等众多优点已逐渐代替了WebForm模型.能够灵活使用MVC提供的扩展点可以达到事半功倍的效果,另一方面Asp.net MVC优秀的设计和高质量的代码也值得我们去 ...
- MySQL 常用的UPDATE操作
标签:UPDATE 概述 测试环境:mysql 5.6.21 步骤 创建测试表 CREATE TABLE `product` ( `proID` ) NOT NULL AUTO_INCREMENT C ...
- php单条件查询,关键字查询
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- 30个你必须记住的CSS选择符
所以你学会了基础的id,类和后代选择符,然后你就一直用它们了吗?如果是这样,你丢失了(css的)巨大的灵活性.在本文中提到的很多选择器属于CSS3规范的一部分,因此,只有在现代浏览器中才可使用. 1. ...
- Leetcode-190 Reverse Bits
#190. Reverse Bits Reverse bits of a given 32 bits unsigned integer. For example, given input 432615 ...
- 使用 DBCC IND 查看BTree 和 Heap结构
Table 有两种结构:BTree 和 Heap,一个table的结构必定是其中之一.如果在表上创建clustered index,那么表的结构是BTree:如果表上没有创建Clustered ind ...