OAuth打造webapi認證服務


使用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空間中的日志。

  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

    icrosoft.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請求的地址,即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模式需要請求頭必須包含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

 

參考:

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/

 

 

 

 

分類:  .NET


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM