在上篇文章介紹了Web Api中使用令牌進行受權的後端實現方法,基於WebApi2和OWIN OAuth實現了獲取access token,使用token訪問需受權的資源信息。本文將介紹在Web Api中啓用刷新令牌的後端實現。html
本文要用到上篇文章所使用的代碼,代碼編寫環境爲VS 201七、.Net Framework 4.7.2,數據庫爲MS SQL 2008 R2.git
上文已經搞了一套Token受權訪問,這裏有多出來一個刷新令牌(Refresh Token),平白添加程序的複雜度,意義何在呢angularjs
刷新令牌(Refresh Token) 是用來從身份認證服務器交換得到新的訪問令牌,容許在沒有用戶交互的狀況下請求新的訪問令牌。刷新令牌有幾個好處:github
這個在沒有特定的業務場景比較難理解,下面仍是一步一步操做一遍,動動手後會有更多收穫。本文須要使用進行客戶端和刷新令牌的持久化,須要用到EF和數據庫客戶端。web
install-package Entityframework
using System.Data.Entity; namespace OAuthExample.Entities { public class OAuthContext : DbContext { public OAuthContext() : base("OAuthConnection") { } public DbSet<Client> Clients { get; set; } public DbSet<RefreshToken> RefreshTokens { get; set; } } }
Client 實體:算法
using System.ComponentModel.DataAnnotations; namespace OAuthExample.Entities { public class Client { [Key] public string Id { get; set; } [Required] public string Secret { get; set; } [Required] [MaxLength(100)] public string Name { get; set; } public ApplicationTypes ApplicationType { get; set; } public bool Active { get; set; } public int RefreshTokenLifeTime { get; set; } [MaxLength(100)] public string AllowedOrigin { get; set; } } public enum ApplicationTypes { JavaScript = 0, NativeConfidential = 1 }; }
RefreshToken 實體:數據庫
using System; using System.ComponentModel.DataAnnotations; namespace OAuthExample.Entities { public class RefreshToken { [Key] public string Id { get; set; } [Required] [MaxLength(50)] public string Subject { get; set; } [Required] [MaxLength(50)] public string ClientId { get; set; } public DateTime IssuedUtc { get; set; } public DateTime ExpiresUtc { get; set; } [Required] public string ProtectedTicket { get; set; } } }
PM> enable-migrations PM> add-migration initDatabase PM> update-database
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using OAuthExample.Entities; namespace OAuthExample.Infrastructure { public class AuthRepository : IDisposable { private OAuthContext context; public AuthRepository() { context = new OAuthContext(); } public Client FindClient(string clientId) { var client = context.Clients.Find(clientId); return client; } public async Task<bool> AddRefreshToken(RefreshToken token) { var existingToken = context.RefreshTokens.Where(r => r.Subject == token.Subject && r.ClientId == token.ClientId).SingleOrDefault(); if (existingToken != null) { var result = await RemoveRefreshToken(existingToken); } context.RefreshTokens.Add(token); return await context.SaveChangesAsync() > 0; } public async Task<bool> RemoveRefreshToken(string refreshTokenId) { var refreshToken = await context.RefreshTokens.FindAsync(refreshTokenId); if (refreshToken != null) { context.RefreshTokens.Remove(refreshToken); return await context.SaveChangesAsync() > 0; } return false; } public async Task<bool> RemoveRefreshToken(RefreshToken refreshToken) { context.RefreshTokens.Remove(refreshToken); return await context.SaveChangesAsync() > 0; } public async Task<RefreshToken> FindRefreshToken(string refreshTokenId) { var refreshToken = await context.RefreshTokens.FindAsync(refreshTokenId); return refreshToken; } public List<RefreshToken> GetAllRefreshTokens() { return context.RefreshTokens.ToList(); } public void Dispose() { context.Dispose(); } } }
到這裏終於完成了Client與RefreshToken兩個實體表的管理,這裏主要是實現一下Client和RefreshToken這兩個實體的一些增刪改查操做,在後面會用到。具體實現方式不限於以上代碼。json
這裏有個小插曲,執行數據遷移的時候出現錯誤 「沒法將參數綁定到參數「Path」,由於該參數是空值。」,卡了半個小時也沒解決,最後卸載掉當前EntityFramework,換了個低版本的,一切正常了。後端
如今,咱們須要實現負責驗證發送給應用程序請求訪問令牌或使用刷新令牌的客戶端信息的客戶端信息的邏輯,所以打開文件「 CustomAuthorizationServerProvider」修改方法ValidateClientAuthentication,代碼以下:api
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId = string.Empty; string clientSecret = string.Empty; Client client = null; if (!context.TryGetBasicCredentials(out clientId, out clientSecret)) { context.TryGetFormCredentials(out clientId, out clientSecret); } if (context.ClientId == null) { //Remove the comments from the below line context.SetError, and invalidate context //if you want to force sending clientId/secrects once obtain access tokens. context.Validated(); //context.SetError("invalid_clientId", "ClientId should be sent."); return Task.FromResult<object>(null); } using (AuthRepository _repo = new AuthRepository()) { client = _repo.FindClient(context.ClientId); } if (client == null) { context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId)); return Task.FromResult<object>(null); } if (client.ApplicationType == ApplicationTypes.NativeConfidential) { if (string.IsNullOrWhiteSpace(clientSecret)) { context.SetError("invalid_clientId", "Client secret should be sent."); return Task.FromResult<object>(null); } else { if (client.Secret != Helper.GetHash(clientSecret)) { context.SetError("invalid_clientId", "Client secret is invalid."); return Task.FromResult<object>(null); } } } if (!client.Active) { context.SetError("invalid_clientId", "Client is inactive."); return Task.FromResult<object>(null); } context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin); context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); context.Validated(); return Task.FromResult<object>(null); }
上述操做,咱們主要執行了一下驗證步驟
- 在請求標頭中獲取clientId 和Client Secret
- 檢查Client Id是否爲空
- 檢查Client是否註冊
- 檢查Client是否須要進行Client Secret驗證的客戶端類型,在此上下文中,NativeConfidential時須要驗證Javascript不須要驗證
- 檢查Client是否處於活動狀態
- 以上全部均爲有效時,將上下文標記爲有效,驗證經過
如今,咱們須要修改方法「 GrantResourceOwnerCredentials」以驗證資源全部者的用戶名/密碼是否正確,並將客戶端ID綁定到生成的訪問令牌,所以打開文件「 CustomAuthorizationServerProvider」並修改GrantResourceOwnerCredentials方法和添加TokenEndpoint實現代碼:
/// <summary> /// Called when a request to the Token endpoint arrives with a "grant_type" of "password". This occurs when the user has provided name and password /// credentials directly into the client application's user interface, and the client application is using those to acquire an "access_token" and /// optional "refresh_token". If the web application supports the /// resource owner credentials grant type it must validate the context.Username and context.Password as appropriate. To issue an /// access token the context.Validated must be called with a new ticket containing the claims about the resource owner which should be associated /// with the access token. The application should take appropriate measures to ensure that the endpoint isn’t abused by malicious callers. /// The default behavior is to reject this grant type. /// See also http://tools.ietf.org/html/rfc6749#section-4.3.2 /// </summary> /// <param name="context">The context of the event carries information in and results out.</param> public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin"); if (allowedOrigin == null) { allowedOrigin = "*"; } context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin }); //這裏是驗證用戶名和密碼,能夠根據項目狀況本身實現 if (!(context.UserName == "zhangsan" && context.Password == "123456")) { 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("sub", context.UserName)); identity.AddClaim(new Claim("role", "user")); var props = new AuthenticationProperties(new Dictionary<string, string> { { "as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId }, { "userName", context.UserName } }); var ticket = new AuthenticationTicket(identity, props); context.Validated(ticket); } /// <summary> /// Called at the final stage of a successful Token endpoint request. An application may implement this call in order to do any final /// modification of the claims being used to issue access or refresh tokens. This call may also be used in order to add additional /// response parameters to the Token endpoint's json response body. /// </summary> /// <param name="context">The context of the event carries information in and results out.</param> /// <returns> /// Task to enable asynchronous execution /// </returns> public override Task TokenEndpoint(OAuthTokenEndpointContext context) { foreach (var item in context.Properties.Dictionary) { context.AdditionalResponseParameters.Add(item.Key, item.Value); } return Task.FromResult<object>(null); }
經過上面代碼,咱們完成了以下操做:
- 從OWIN中獲取到「Access-Control-Allow-Origin」並添加到OWIN上下文響應中
- 驗證資源全部者的用戶名/密碼,這裏是寫死的,實際應用中能夠本身擴展一下,驗證完成後調用context.Validated(ticket),將生成token
如今咱們須要生成Refresh Token並實現持久化到數據庫中,咱們須要添加一個新的實現類。項目中找到Providers文件夾,右鍵添加類,命名爲」CustomRefreshTokenProvider」,該類繼承於」IAuthenticationTokenProvider」,實現代碼以下:
using System; using System.Threading.Tasks; using log4net; using Microsoft.Owin.Security.Infrastructure; using OAuthExample.Entities; using OAuthExample.Infrastructure; namespace OAuthExample.Providers { public class CustomRefreshTokenProvider : IAuthenticationTokenProvider { private ILog logger = LogManager.GetLogger(typeof(CustomRefreshTokenProvider)); public void Create(AuthenticationTokenCreateContext context) { throw new NotImplementedException(); } /// <summary> /// Creates the asynchronous. /// </summary> /// <param name="context">The context.</param> public async Task CreateAsync(AuthenticationTokenCreateContext context) { var clientid = context.Ticket.Properties.Dictionary["as:client_id"]; if (string.IsNullOrEmpty(clientid)) { return; } //爲刷新令牌生成一個惟一的標識符,這裏咱們使用Guid,也能夠本身單獨寫一個字符串生成的算法 var refreshTokenId = Guid.NewGuid().ToString("n"); using (AuthRepository _repo = new AuthRepository()) { //從Owin上下文中讀取令牌生存時間,並將生存時間設置到刷新令牌 var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime"); var token = new RefreshToken() { Id = Helper.GetHash(refreshTokenId), ClientId = clientid, Subject = context.Ticket.Identity.Name, IssuedUtc = DateTime.UtcNow, ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime)) }; //爲刷新令牌設置有效期 context.Ticket.Properties.IssuedUtc = token.IssuedUtc; context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc; //負責對票證內容進行序列化,稍後咱們將次序列化字符串持久化到數據 token.ProtectedTicket = context.SerializeTicket(); var result = await _repo.AddRefreshToken(token); //在響應中文中發送刷新令牌Id if (result) { context.SetToken(refreshTokenId); } } } public void Receive(AuthenticationTokenReceiveContext context) { throw new NotImplementedException(); } /// <summary> /// Receives the asynchronous. /// </summary> /// <param name="context">The context.</param> /// <returns></returns> /// <exception cref="System.NotImplementedException"></exception> public async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { //設置跨域訪問 var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin"); context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin }); //獲取到刷新令牌,hash後在數據庫查找是否已經存在 string hashedTokenId = Helper.GetHash(context.Token); 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); } } } } }
經過上面代碼,咱們完成了以下操做:
- 在CreateAsync 方法中
咱們在此方法實現了刷新令牌的設置和生成,並持久化到數據。添加此方法後,在獲取access token的過程當中,須要將client Id添加到Authorization中,驗證經過後,在響應報文中生成了refresh_token
- 在ReceiveAsync 方法中
- 咱們在此方法實現了經過刷新令牌獲取訪問令牌的一部分,詳見代碼註釋
打開CustomRefreshTokenProvider類,添加實現接口方法ReceiveAsync 。代碼見上
打開CustomAuthorizationServerProvider類,添加GrantRefreshToken方法的實現,代碼以下:
/// <summary> /// Called when a request to the Token endpoint arrives with a "grant_type" of "refresh_token". This occurs if your application has issued a "refresh_token" /// along with the "access_token", and the client is attempting to use the "refresh_token" to acquire a new "access_token", and possibly a new "refresh_token". /// To issue a refresh token the an Options.RefreshTokenProvider must be assigned to create the value which is returned. The claims and properties /// associated with the refresh token are present in the context.Ticket. The application must call context.Validated to instruct the /// Authorization Server middleware to issue an access token based on those claims and properties. The call to context.Validated may /// be given a different AuthenticationTicket or ClaimsIdentity in order to control which information flows from the refresh token to /// the access token. The default behavior when using the OAuthAuthorizationServerProvider is to flow information from the refresh token to /// the access token unmodified. /// See also http://tools.ietf.org/html/rfc6749#section-6 /// </summary> /// <param name="context">The context of the event carries information in and results out.</param> /// <returns> /// Task to enable asynchronous execution /// </returns> public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context) { //從原始票證中讀取Client Id與當前的Client Id進行比較,若是不一樣,將拒絕次操做請求,以保證刷新令牌生成後綁定到同一個Client var originalClient = context.Ticket.Properties.Dictionary["as:client_id"]; var currentClient = context.ClientId; if (originalClient != currentClient) { context.SetError("invalid_clientId", "Refresh token is issued to a different clientId."); return Task.FromResult<object>(null); } // Change auth ticket for refresh token requests var newIdentity = new ClaimsIdentity(context.Ticket.Identity); var newClaim = newIdentity.Claims.Where(c => c.Type == "newClaim").FirstOrDefault(); if (newClaim != null) { newIdentity.RemoveClaim(newClaim); } newIdentity.AddClaim(new Claim("newClaim", "newValue")); var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties); //方法執行後,代碼將回到CustomRefreshTokenProvider中的"CreateAsync"方法,生成新的刷新令牌,並將其和新的訪問令牌一塊兒返回到響應中 context.Validated(newTicket); return Task.FromResult<object>(null); }
使用PostMan進行模擬測試
在未受權時,訪問 http://localhost:56638/api/Orders,提示「已拒絕爲此請求受權」
獲取受權,訪問 http://localhost:56638/oauth/token,得到的報文中包含有refresh_token
使用Refresh token獲取新的Access token
使用新的Access Token 附加到Order請求,再次嘗試訪問:
監視請求上下文中的信息以下,注意newClaim是刷新令牌訪問時才設置的:
dddd,終於把這兩個總結搞完了,回頭把以前webapi參數加密的合到一塊兒,代碼整理一下放到文末。
本文參照了不少文章以及代碼,文章主要架構與下面連接基本一致,其實也沒多少原創,可是在整理總結的過程當中梳理了一邊Access Token 和Refresh Token的知識,當你在組織語言解釋代碼的時候,才無情的揭露了本身的無知與淺薄,受益不淺~
Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin
其餘參考資料是在較多,有的看一點就關掉了。有的做爲參考,列舉一二
Web API與OAuth:既生access token,何生refresh token
在ASP.NET中基於Owin OAuth使用Client Credentials Grant受權發放Token
ASP.NET Web API與Owin OAuth:調用與用戶相關的Web API
C#進階系列——WebApi 身份認證解決方案:Basic基礎認證
最後本文示例代碼地址:Github