首页 > 解决方案 > 仍登录 MVC 站点,但无法调用 Web API

问题描述

我有一个 ASP.NET MVC 站点、IdentityServer4 主机和一个 Web API。

当我使用外部提供商 (Facebook) 登录 MVC 站点时,我已正常登录。从 MVC 站点,我也可以正确使用 Web API。

但是,第二天,我仍然登录到 MVC 站点,但是当我尝试访问 Web API 时,我得到一个“未授权异常”。

因此,尽管我仍然在 MVC 站点中登录,但我不再通过身份验证来从 MVC 站点中调用 Web API。

我想知道如何处理这种情况,以及应如何配置 IdentityServer4。

MVC 应用程序的配置如下:

 services.AddAuthentication(options =>
        {
            options.DefaultScheme = "Cookies";
            options.DefaultChallengeScheme = "oidc"; 
        })
        .AddCookie("Cookies")
        .AddOpenIdConnect("oidc", options =>
        {
            options.SignInScheme = "Cookies";
            options.Authority = mgpIdSvrSettings.Authority;
            options.RequireHttpsMetadata = false;                
            options.ClientId = mgpIdSvrSettings.ClientId;
            options.ClientSecret = mgpIdSvrSettings.ClientSecret; // Should match the secret at IdentityServer
            options.ResponseType = "code id_token"; // Use hybrid flow
            options.SaveTokens = true;                
            options.GetClaimsFromUserInfoEndpoint = true;                
            options.Scope.Add("mgpApi");
            options.Scope.Add("offline_access");                  
        });            

所以它使用混合流。

在 IdentityServer 中,MVC 客户端的配置如下:

new Client
{
     EnableLocalLogin = false,

     ClientId = "mgpPortal",
     ClientName = "MGP Portal Site",
     AllowedGrantTypes = GrantTypes.Hybrid,

     // where to redirect to after login
     RedirectUris = mgpPortalSite.RedirectUris,

     // where to redirect to after logout
     PostLogoutRedirectUris = mgpPortalSite.PostLogoutRedirectUris,

     // secret for authentication
     ClientSecrets = mgpPortalSite.ClientSecrets.Select(cs => new Secret(cs.Sha256())).ToList(),

     AllowedScopes = new List<string>
     {
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            "mgpApi"
     },

     AllowOfflineAccess = true,                             
     RequireConsent = false,
},

最后是 Web API:

 services.AddAuthentication("Bearer")                
           .AddIdentityServerAuthentication(options =>
            {
                options.Authority = mgpIdSvrSettings.Authority;
                options.RequireHttpsMetadata = false;                    
                options.ApiName = mgpIdSvrSettings.ApiName;
                options.EnableCaching = true;
                options.CacheDuration = TimeSpan.FromMinutes(10);                    
            });

标签: c#asp.net-core-mvcidentityserver4asp.net-core-webapi

解决方案


有两种类型的身份验证,cookie 和承载。

在 cookie 让您登录的地方,不记名令牌不能。因为不记名令牌被设置为在某个时候过期,而不允许您更改生命周期。

访问令牌过期后访问资源 (api) 的唯一方法是让用户再次登录或使用刷新令牌请求新的访问令牌,而无需用户交互。

您已经配置了它:

options.Scope.Add("offline_access");

在每次登录时,请求将至少包含一个刷新令牌。将其存放在安全的地方,并在需要时使用。默认情况下,它设置为仅使用一次。


您可以使用类似此代码的代码来更新令牌(因为您实际上并没有刷新它,而是替换它)。您需要包含“IdentityModel”NuGet 包,如 IdentityServer 的示例中所示。

private async Task<TokenResponse> RenewTokensAsync()
{
    // Initialize the token endpoint:
    var client = _httpClientFactory.CreateClient();
    var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");

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

    // Read the stored refresh token:
    var rt = await HttpContext.GetTokenAsync("refresh_token");
    var tokenClient = _httpClientFactory.CreateClient();

    // Request a new access token:
    var tokenResult = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
    {
        Address = disco.TokenEndpoint,

        ClientId = "mvc",
        ClientSecret = "secret",
        RefreshToken = rt
    });

    if (!tokenResult.IsError)
    {
        var old_id_token = await HttpContext.GetTokenAsync("id_token");
        var new_access_token = tokenResult.AccessToken;
        var new_refresh_token = tokenResult.RefreshToken;
        var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);

        // Save the information in the cookie
        var info = await HttpContext.AuthenticateAsync("Cookies");

        info.Properties.UpdateTokenValue("refresh_token", new_refresh_token);
        info.Properties.UpdateTokenValue("access_token", new_access_token);
        info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));

        await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
        return tokenResult;
    }
    return null;
}

默认情况下,刷新令牌使用配置为一次性使用。请注意,当存储新刷新令牌失败并且您应该丢失它时,请求新刷新令牌的唯一方法是强制用户再次登录。

另请注意,刷新令牌可能会过期。


退后一步,当访问令牌过期或即将过期时,您需要使用它:

var accessToken = await HttpContext.GetTokenAsync("access_token");

var tokenHandler = new JwtSecurityTokenHandler();

var jwtSecurityToken = tokenHandler.ReadJwtToken(accessToken);

// Depending on the lifetime of the access token.
// This is just an example. An access token may be valid
// for less than one minute.
if (jwtSecurityToken.ValidTo < DateTime.UtcNow.AddMinutes(5))
{
    var responseToken = await RenewTokensAsync();
    if (responseToken == null)
    {
        throw new Exception("Error");
    }
    accessToken = responseToken.AccessToken;
}

// Proceed, accessToken contains a valid token.

推荐阅读