8-OAuth2 & OpenID Connect & Asp net core Token生命周期和刷新

96
俊果果
2018.08.26 11:13 字数 153

一、Token 的生命周期

image.png

Identity token 用于登录客户端;通常来说,如果用户在程序中还是活跃的,那么应该保持登录状态
Access token 的过期由 IDP Policy 控制,默认一小时有效

二、设置 Token 时限示例

  • 修改 IDP 的 Config::GetClients(), 为 new client 添加如下代码
 //以下 token 的时间单位都是秒
                    IdentityTokenLifetime = 5 * 60, // 默认就是五分钟,所以这里也可以注释掉
                    AuthorizationCodeLifetime = 5 * 60, // 默认五分钟
                    AccessTokenLifetime = 2 * 60, // 默认 一小时。因为 IDP 有自己的刷新机制(5分钟间隔),所以这个 token 实际过期的时间也许比两分钟长
                    

运行后,进入主页,等待两分钟或更久,再刷新页面,会提示没有访问权限

三、获取长期有效的 token

image.png

使用 Refresh token 来获取新的 Access token 可以避免token过期提示以及让用户重新输入登录条件
客户端发送给 IDP 一个 post 请求,需要把 clientid 和 secret key 作为 authorization header 添加到 http 请求头中,同时添加 Refresh token 作为请求 body。 IDP 收到请求验证通过后,会返回新的 identity token 和 access token(以及可选的 refresh token)
支持 refresh token 需要 IDP 启用 offline_access scope
示例:

  • 修改 IDP 的 Config::GetClients(), 为 new client 添加如下代码
AllowOfflineAccess = true, // 启用 'offline_access' scope
AbsoluteRefreshTokenLifetime = 30 * 24 * 60 * 60, // refresh token 的绝对过期时间,默认30天
RefreshTokenExpiration = TokenExpiration.Sliding, // refresh token 一般不需要设置绝对的过期时间,设置成 sliding 模式就好
SlidingRefreshTokenLifetime = 5 * 60,//  sliding 模式下,当请求新的 refresh token时,他的过期时间会被重置为这里设置的值(但不会超过 AbsoluteRefreshTokenLifetime 的设置)
UpdateAccessTokenClaimsOnRefresh = true, // refresh token 请求是否更新 access token 里面携带的 user claim 信息;设为 true, 即使 token 没有过期,也会更新 accesstoken 的 claim 值

+修改 Client::Startup类的 ConfigureServices,添加 scope

options.Scope.Add("offline_access");
  • 修改 ImageClient类为如下
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace ImageGallery.Client.Services
{
    public class ImageGalleryHttpClient : IImageGalleryHttpClient
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private HttpClient _httpClient = new HttpClient();

        public ImageGalleryHttpClient(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
        
        public async Task<HttpClient> GetClient()
        {
            var accessToken = string.Empty;
            // get the current HttpContext to access the tokens
            var currentContext = _httpContextAccessor.HttpContext;
            
            var expires_at = await currentContext.GetTokenAsync("expires_at");
            if (string.IsNullOrWhiteSpace(expires_at)
                || DateTime.Parse(expires_at).AddSeconds(-60).ToUniversalTime() < DateTime.UtcNow)
            {
                // token 过期时间未设置,或者当前距离过期时间小于 60s
                accessToken = await RenewTokens();
            }
            else
            {
                // get access token from currentContext
                accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            }

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                // set as Bearer token
                _httpClient.SetBearerToken(accessToken);
            }

            _httpClient.BaseAddress = new Uri("https://localhost:44363/");
            _httpClient.DefaultRequestHeaders.Accept.Clear();
            _httpClient.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));

            return _httpClient;
        }

        private async Task<string> RenewTokens()
        {
            // get the current HttpContext to access the tokens
            var currentContext = _httpContextAccessor.HttpContext;

            // get metadata
            var discoveryClient = new DiscoveryClient("https://localhost:44319/");
            var metaDataResponse = await discoveryClient.GetAsync(); // 结果包含 token endpoint 的 uri

            // create a new token client to get new tokens
            var tokenClient = new TokenClient(metaDataResponse.TokenEndpoint, "junguoguoimagegalleryclient", "junguoguosecret");

            // get the saved refresh token
            var currentRefreshToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

            // refresh the tokens
            var tokenResult = await tokenClient.RequestRefreshTokenAsync(currentRefreshToken);

            if (!tokenResult.IsError)
            {
                // update the tokens & expiration value
                // 将新返回的 accesstoken, idtoken, refreshtoken 存下来
                var updatedTokens = new List<AuthenticationToken>();
                updatedTokens.Add(new AuthenticationToken()
                {
                    Name = OpenIdConnectParameterNames.IdToken,
                    Value = tokenResult.IdentityToken
                });
                updatedTokens.Add(new AuthenticationToken()
                {
                    Name = OpenIdConnectParameterNames.AccessToken,
                    Value = tokenResult.AccessToken
                });
                updatedTokens.Add(new AuthenticationToken()
                {
                    Name = OpenIdConnectParameterNames.RefreshToken,
                    Value = tokenResult.RefreshToken
                });

                // 存储更新的 access token 过期时间
                var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                updatedTokens.Add(new AuthenticationToken()
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o",CultureInfo.InvariantCulture) // 格式类似:"2018-08-26T02:42:05.8636746Z"
                });

                // get autenticate result, containing the current principal & properties
                // 获取 cookie 并更新
                var currentAuthenticationResult = await currentContext.AuthenticateAsync("Cookies");
                // store the updated tokens
                currentAuthenticationResult.Properties.StoreTokens(updatedTokens);

                // sign in
                await currentContext.SignInAsync("Cookies", currentAuthenticationResult.Principal,
                    currentAuthenticationResult.Properties);
                // return the new access token
                return tokenResult.AccessToken;
            }
            else
            {
                //throw new Exception("Problem encountered while refreshing tokens", tokenResult.Exception);
                return ""; // 不抛异常而是直接返回空值,会提示 AccessDenied 页面
            }
        }
    }
}

如此,运行网站,等待超时时间后,会重新获取 token 而不是提示无权访问

github commit 点击这里查看

Web note ad 1