最近同事用iOS App調用Open API時遇到一個問題:在access token過時後,用refresh token刷新access token時,服務器響應"invalid_grant"錯誤;而在access token沒有過時的狀況下,能正常刷新access token。html
先查看了一下OAuth規範中的「Refreshing an Expired Access Token」流程圖,以確認客戶端的操做流程有沒有問題。數據庫
問題發生在上圖中的(G)操做步驟。iOS App就是按上圖的流程進行操做的——在調用Open API時,若是服務器響應access token無效,就用refresh token刷新access token,客戶端的操做流程一點問題沒有。服務器
因而將問題鎖定在服務端。打點寫日誌,發現refresh token刷新access token時服務端先調用了IAuthenticationTokenProvider.ReceiveAsync(),而後調用了IOAuthAuthorizationServerProvider.GrantRefreshToken()方法。對應於咱們的實現就是CNBlogsRefreshTokenProvider.ReceiveAsync()與CNBlogsAuthorizationServerProvider.GrantRefreshToken()。async
查看ReceiveAsync()方法的實現代碼:ide
public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { var refreshToken = await _refreshTokenService.Get(context.Token); if (refreshToken != null) { context.DeserializeTicket(refreshToken.ProtectedTicket); await _refreshTokenService.Remove(context.Token); } }
從上面的代碼能夠得知,用refresh token刷新access token,實際就是將存儲在數據庫中的ProtectedTicket反序列爲包含access token的ticket,而後根據這個ticket生成access token返回給客戶端。而refreshToken.ProtectedTicket是在生成refresh token時由包含access token的ticket序列化生成的,refresh token沒法刷新access token,問題極可能就出在它身上。ui
因而查看生成refresh token部分的代碼——CNBlogsRefreshTokenProvider(繼承自AuthenticationTokenProvider)的CreateAsync()方法:spa
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() }; context.Ticket.Properties.IssuedUtc = refreshToken.IssuedUtc; context.Ticket.Properties.ExpiresUtc = refreshToken.ExpiresUtc;
當時一看代碼,立馬恍然大悟。應該是先設置IssuedUtc與ExpiresUtc,而後再SerializeTicket();實際被寫成了先SerializeTicket(),後設置IssuedUtc與ExpiresUtc。結果形成refreshToken.ProtectedTicket的過時時間不正確,從而沒法刷新access token。日誌
解決方法很簡單,只需將context.SerializeTicket()移至設置Ticket的IssuedUtc與ExpiresUtc的代碼以後:code
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)), }; context.Ticket.Properties.IssuedUtc = refreshToken.IssuedUtc; context.Ticket.Properties.ExpiresUtc = refreshToken.ExpiresUtc; refreshToken.ProtectedTicket = context.SerializeTicket();