標籤:
之前寫過2篇關於refresh token的產生與持久化的博文:1)Web API與OAuth:既生access token,何生refresh token;2)ASP.NET OWIN OAuth:refresh token的持久化。
之後我們在CNBlogsRefreshTokenProvider中這樣實現了refresh token的產生與持久化:
public class CNBlogsRefreshTokenProvider : AuthenticationTokenProvider{ private IRefreshTokenService _refreshTokenService; public CNBlogsRefreshTokenProvider(IRefreshTokenService refreshTokenService) { _refreshTokenService = refreshTokenService; } public override async Task CreateAsync(AuthenticationTokenCreateContext context) { if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return; var clientId = context.OwinContext.Get<string>("as:client_id"); if (string.IsNullOrEmpty(clientId)) return; var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime"); if (string.IsNullOrEmpty(refreshTokenLifeTime)) return; //generate access token RandomNumberGenerator cryptoRandomDataGenerator = new RNGCryptoServiceProvider(); byte[] buffer = new byte[60]; cryptoRandomDataGenerator.GetBytes(buffer); var refreshTokenId = Convert.ToBase64String(buffer).TrimEnd(‘=‘).Replace(‘+‘, ‘-‘).Replace(‘/‘, ‘_‘); var refreshToken = new RefreshToken() { Id = refreshTokenId, ClientId = new Guid(clientId), UserName = context.Ticket.Identity.Name, IssuedUtc = DateTime.UtcNow, ExpiresUtc = DateTime.UtcNow.AddSeconds(Convert.ToDouble(refreshTokenLifeTime)), ProtectedTicket = context.SerializeTicket(), IP = context.Request.GetUserIp() }; context.Ticket.Properties.IssuedUtc = refreshToken.IssuedUtc; context.Ticket.Properties.ExpiresUtc = refreshToken.ExpiresUtc; if (await _refreshTokenService.Save(refreshToken)) { context.SetToken(refreshTokenId); } } public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { var refreshToken = await _refreshTokenService.Get(context.Token); if (refreshToken != null) { context.DeserializeTicket(refreshToken.ProtectedTicket); var result = await _refreshTokenService.Remove(context.Token); } }}
CNBlogsRefreshTokenProvider
後來發現一個問題(這是遇到的第1個問題),在使用者不登入的情況下,以client credentials grant方式擷取access token時,也會產生refresh token並且儲存至資料庫。而refresh token是為瞭解決以resource owner password credentials grant方式擷取access token時多次輸入使用者名稱與密碼的麻煩。所以,對於client credentials grant的情境,產生refresh token完全沒有必要。
於是,就得想辦法避免這種refresh token生不逢時的情況。後來,找到瞭解決方法,很簡單,只需在CreateAsync的重載方法的開頭加上如下的代碼:
public class CNBlogsRefreshTokenProvider : AuthenticationTokenProvider{ public override async Task CreateAsync(AuthenticationTokenCreateContext context) { if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return; //... }}
遇到的第2個問題是,Client多次以resource owner password credentials grant的方式擷取refresh token,會產生多個refresh token,並且會在資料庫中儲存多條記錄。
通常情況的操作是,Client以resource owner password credentials grant的方式擷取refresh token,並之將儲存。需要更新access token時就用這個refresh token去更新,更新的同時會產生新的refresh token,並且將原先的refresh token刪除。對應的實現代碼如下:
public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context){ var refreshToken = await _refreshTokenService.Get(context.Token); if (refreshToken != null) { context.DeserializeTicket(refreshToken.ProtectedTicket); var result = await _refreshTokenService.Remove(context.Token); }}
但是當Client多次擷取多個refresh token時,只有那個用於重新整理access token的refresh token會被刪除,其他的refresh token會成為無人問津的垃圾留在資料庫中。為了愛護環境,不亂扔垃圾,我們得解決這個問題。
解決的思路是在產生新的refresh token並將之儲存至資料庫之前,將對應於這個使用者(resource owner)及這個client的所有refresh token刪除。刪除所依據的條件是ClientId與UserId,由於之前持久化refresh token時只儲存了UserName,沒有儲存UserId,所以要給RefreshToken增加UserId屬性。然後給Application層的IRefreshTokenService介面增加刪除方法:
public interface IRefreshTokenService{ //... Task<bool> Remove(Guid clientId, Guid userId);}
(該方法的實現省略)
接著在CNBlogsRefreshTokenProvider中儲存refresh token之前,調用這個方法:
public class CNBlogsRefreshTokenProvider : AuthenticationTokenProvider{ private IRefreshTokenService _refreshTokenService; public override async Task CreateAsync(AuthenticationTokenCreateContext context) { var refreshToken = new RefreshToken() { //... UserId = (await UCenterService.GetUser(context.Ticket.Identity.Name)).UserID, //... }; await _refreshTokenService.Remove(refreshToken.ClientId, refreshToken.UserId); if (await _refreshTokenService.Save(refreshToken)) { context.SetToken(refreshTokenId); } }}
這樣就解決了第2個問題。
ASP.NET OWIN OAuth:遇到的2個refresh token問題