Web API與OAuth:既生access token,何生refresh token


在前一篇博文中,我們基於 ASP.NET Web API 與 OWIN OAuth 以 Resource Owner Password Credentials Grant 的授權方式( grant_type=password )獲取到了 access token,並以這個 token 成功調用了與當前用戶(resource owner)關聯的 Web API。

本以為搞定了 access token 就搞定了 Web API 的驗證與授權問題,可是發現 OAuth 中還有一種 token,叫 refresh token。開始的時候很是納悶,access token 已經能解決問題,為什么要搞定兩套 token,refresh token 有啥用?在納悶之下,發出了這樣的感慨:既生 access token,何生 refresh token?

后來看了一些資料,有點明白了。refresh token 是專用於刷新 access token 的 token。

為什么要刷新 access token 呢?一是因為 access token 是有過期時間的,到了過期時間這個 access token 就失效,需要刷新;二是因為一個 access token 會關聯一定的用戶權限,如果用戶授權更改了,這個 access token 需要被刷新以關聯新的權限。

為什么要專門用一個 token 去更新 access token 呢?如果沒有 refresh token,也可以刷新 access token,但每次刷新都要用戶輸入登錄用戶名與密碼,多麻煩。有了 refresh token,可以減少這個麻煩,客戶端直接用 refresh token 去更新 access token,無需用戶進行額外的操作。

兩個為什么也許沒有解釋清楚 refresh token 的用途,下面我們用示例代碼在 ASP.NET Web API 與 OWIN OAuth 中實際體驗一下,或許有更直觀的認識。

(一)Refresh token 的生成、發放、保存

實現一個 RefreshTokenProvider ,比如 CNBlogsRefreshTokenProvider。

需要重載 Microsoft.Owin.Security.Infrastructure.AuthenticationTokenProvider 中的 Create() 與 Receive() 方法(或者直接實現 IAuthenticationTokenProvider 接口),示例代碼如下:

public class CNBlogsRefreshTokenProvider : AuthenticationTokenProvider
{
    private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>();

    public override void Create(AuthenticationTokenCreateContext context)
    {
        string tokenValue = Guid.NewGuid().ToString("n");

        context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;        
        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60);

        _refreshTokens[tokenValue] = context.SerializeTicket();

        context.SetToken(tokenValue);
    }

    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        string value;
        if (_refreshTokens.TryRemove(context.Token, out value))
        {
            context.DeserializeTicket(value);
        }
    }
}

(注:后來采用的是重載CreateAsync()方法)

然后應用這個 CNBlogsRefreshTokenProvider:

public void ConfigureAuth(IAppBuilder app)
{
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/token"),
        Provider = new CNBlogsAuthorizationServerProvider(),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
        AllowInsecureHttp = true,
        RefreshTokenProvider = new CNBlogsRefreshTokenProvider()
    };

    app.UseOAuthBearerTokens(OAuthOptions);
}

(二)驗證持有 refresh token 的客戶端

重載 OAuthAuthorizationServerProvider.GrantRefreshToken() 方法,示例代碼如下:

using Microsoft.Owin.Security.OAuth;

namespace OpenAPI.Providers
{
    public class CNBlogsAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
        {
            var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
            var currentClient = context.ClientId;

            if (originalClient != currentClient)
            {
                context.Rejected();
                return;
            }

            var newId = new ClaimsIdentity(context.Ticket.Identity);
            newId.AddClaim(new Claim("newClaim", "refreshToken"));

            var newTicket = new AuthenticationTicket(newId, context.Ticket.Properties);
            context.Validated(newTicket);

            await base.GrantRefreshToken(context);
        }
    }
}

為了驗證client_id,需要在 GrantClientCredentials() 重載方法中保存client_id至context.Ticket:

namespace OpenAPI.Providers
{
    public class CNBlogsAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
        {
            var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);

            var props = new AuthenticationProperties(new Dictionary<string, string>
                {
                    { "as:client_id", context.ClientId }
                });
            var ticket = new AuthenticationTicket(oAuthIdentity, props);

            context.Validated(ticket);
        }    
    }
}

只需實現上面這些代碼,其他的都由 Microsoft.Owin.Security.OAuth 幫你代勞了。

(三)測試客戶端獲取 refresh token

客戶端獲取 access token 與 refresh token 是一起的,示例代碼如下:

[Fact]
public async Task GetAccessTokenTest()
{
    var clientId = "[clientId]";
    var clientSecret = "[clientSecret]";

    var parameters = new Dictionary<string, string>();
    parameters.Add("grant_type", "password");            
    parameters.Add("username", "[username]");
    parameters.Add("password", "[password]");

    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Basic",
        Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));

    var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
    var responseValue = await response.Content.ReadAsStringAsync();

    Console.WriteLine(responseValue);
}

運行結果:

{ 
    "access_token": "D3VjxsFvr...",
    "token_type": "bearer",
    "expires_in": 86399,
    "refresh_token": "7f7edd15cba043c29d487235c2276eb1"
}

成功拿到了 access token。

(四)測試客戶端用 refresh token 刷新 access token

客戶端測試代碼如下:

public async Task GetAccessTokenByRefreshTokenTest()
{
    var clientId = "[clientId]";
    var clientSecret = "[clientSecret]";

    var parameters = new Dictionary<string, string>();
    parameters.Add("grant_type", "refresh_token");
    parameters.Add("refresh_token", "7f7edd15cba043c29d487235c2276eb1");

    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Basic",
        Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));

    var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
    var responseValue = await response.Content.ReadAsStringAsync();

    Console.WriteLine(responseValue);
}

注:這段客戶端代碼與前一步中客戶端代碼的主要區別是少了下面傳遞 resource owner 用戶名與密碼的代碼,這就是 refresh token 的用途所在 —— 不需要用戶名與密碼就可以刷新 access token。

parameters.Add("username", "[username]");
parameters.Add("password", "[password]");

運行結果:

{
    "access_token": "[new access token]",
    "token_type": "bearer",
    "expires_in": 86399,
    "refresh_token": "[new refresh token]"
}

搞定!

看起來挺簡單,卻折騰了一天。 希望在你折騰OAuth的時候,這篇博文能夠幫你減少折騰的時間。

【參考資料】

Adding Refresh Tokens to a Web API v2 Authorization Server

EmbeddedResourceOwnerFlowWithRefreshTokens

Katana source code

Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin


免責聲明!

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



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