07 为 MVC 客户端刷新 Token

原文:https://www.yuque.com/yuejiangliu/dotnet/gbzs4g


07 为 MVC 客户端刷新 Token.mp4 (72.6 MB)

本节是上节的补充,主要讲解如何使用 Refresh Token 刷新 Access Token。

一、设置并启用过期时间

打开 Idp 项目,修改 MVC Client 的 AccessTokenLifetime 为 60s:

// MVC client, authorization code
new Client
{
    ...
    // 设为 True 即支持 Refresh Token
    AllowOfflineAccess = true, // offline_access
    AccessTokenLifetime = 60, // 60 seconds

    AllowedScopes =
    {
        ...
    }
},

通过 jwt.io 能够看到过期时间设置在 Token 里面了。

image.png

结果发现 exp 都过了还能从 Api1 获得资源。

实际上是 Api1 中没有及时的验证 Token(默认为 300s 验证一次)。

修改 Api1,Token 验证间隔为 1 分钟,且 Token 必须包含过期时间:

services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;

        options.Audience = "api1";
        options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1);
        options.TokenValidationParameters.RequireExpirationTime = true;
    });

效果:过期后报错 Exception: Unauthorized

image.png

二、Refresh Token

参考 OpenID Connect 协议 构造 RefreshTokenRequest:

image.png

在 MVC Client 的 HomeController 中添加刷新 Token 的方法:

private async Task<string> RenewTokenAsync()
{
    var client = new HttpClient();
    var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");

    if (disco.IsError) throw new Exception(disco.Error);

    var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

    // Refresh Access Token
    var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
    {
        Address = disco.TokenEndpoint,
        ClientId = "mvc client",
        ClientSecret = "mvc secret",
        Scope = "api1 openid profile email phone address",
        GrantType = OpenIdConnectGrantTypes.RefreshToken,
        RefreshToken = refreshToken
    });

    if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);

    var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);

    var tokens = new[]
    {
        new AuthenticationToken
        {
            Name = OpenIdConnectParameterNames.IdToken,
            Value = tokenResponse.IdentityToken
        },
        new AuthenticationToken
        {
            Name = OpenIdConnectParameterNames.AccessToken,
            Value = tokenResponse.AccessToken
        },
        new AuthenticationToken
        {
            Name = OpenIdConnectParameterNames.RefreshToken,
            Value = tokenResponse.RefreshToken
        },
        new AuthenticationToken
        {
            Name = "expires_at",
            Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
        }
    };

    // 获取身份认证的结果,包含当前的 Principal 和 Properties
    var currentAuthenticateResult =
        await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);

    // 更新 Cookie 里面的 Token
    currentAuthenticateResult.Properties.StoreTokens(tokens);

    // 登录
    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
        currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);

    return tokenResponse.AccessToken;
}

ToString("o"): image.png

在 Index Action 中刷新 Token:

public async Task<IActionResult> Index()
{
    var client = new HttpClient();
    var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
    if (disco.IsError) throw new Exception(disco.Error);

    var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

    client.SetBearerToken(accessToken);

    var response = await client.GetAsync("http://localhost:5001/identity");
    if (!response.IsSuccessStatusCode)
    {
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            // 这样写仅为了方便演示
            await RenewTokenAsync();
            return RedirectToAction();
        }

        throw new Exception(response.ReasonPhrase);
    }

    var content = await response.Content.ReadAsStringAsync();
    return View("Index", content);
}

注:IdentityServer4.Samples 项目没有了,推荐参考官方文档 Switching to Hybrid Flow and adding API Access back 中的代码。

posted on 2020-10-26 17:08  springsnow  阅读(396)  评论(0编辑  收藏  举报

导航