使用 .net5.0 使用 IdentityServer4 进行身份验证时大摇大摆地获取 api 的问题

Issues getting an api with swagger to authenticate with IdentityServer4 using .net5.0

我目前正在学习微服务如何适用于我正在为我的投资组合以及需要此特定应用程序的一小群人构建的应用程序。我已经按照在线教程成功地让 IdentityServer4 对 MVC 客户端进行身份验证,但是,我正试图大摇大摆地与 API 一起工作,尤其是当它们需要身份验证时。每次我尝试使用 IdentityServer4 授权 swagger 时,每次我尝试验证时都会收到 invalid_scope 错误。我已经调试这个问题好几个小时了,但我无法找出问题所在。我也以 Microsoft eShopOnContainers 为例,但仍然没有成功。任何帮助将不胜感激。我尽量保持代码示例简短,请索取任何未显示的代码,我会尽力尽快回复。谢谢。

Identiy.API 项目 startup.cs:

public class Startup {
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentityServer()
                .AddInMemoryClients(Config.GetClients())
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryApiScopes(Config.GetApiScopes())
                .AddTestUsers(Config.GetTestUsers())
                .AddDeveloperSigningCredential();   // @note - demo purposes only. need X509Certificate2 for production)

        services.AddControllersWithViews();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        app.UseStaticFiles();
        app.UseIdentityServer();
        app.UseAuthorization();

        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
    }
}

Config.cs(我删除了测试用户以缩短代码,因为它不相关)。 @note - 我创建了一个专门用于 swagger 的 WeatherSwaggerUI 客户端,因为这是微软提供的 eShopOnContainers 示例项目的一部分:

public static class Config
{
    public static List<TestUser> GetTestUsers()
    {
        return new List<TestUser>(); // removed test users for this post
    }

    public static IEnumerable<Client> GetClients()
    {
        // @note - clients can be defined in appsettings.json
        return new List<Client>
        {
            // m2m client credentials flow client
            new Client
               {
               ClientId = "m2m.client",
               ClientName = "Client Credentials Client",

               AllowedGrantTypes = GrantTypes.ClientCredentials,
               ClientSecrets = { new Secret("SuperSecretPassword".ToSha256())},

                AllowedScopes = { "weatherapi.read", "weatherapi.write" }
            },
            // interactive client
            new Client
            {
                ClientId = "interactive",
                ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},

                AllowedGrantTypes = GrantTypes.Code,

                RedirectUris = {"https://localhost:5444/signin-oidc"},
                FrontChannelLogoutUri = "https://localhost:5444/signout-oidc",
                PostLogoutRedirectUris = {"https://localhost:5444/signout-callback-oidc"},

                AllowOfflineAccess = true,
                AllowedScopes = {"openid", "profile", "weatherapi.read"},
                RequirePkce = true,
                RequireConsent = false,
                AllowPlainTextPkce = false
            },
            new Client
            {
                ClientId = "weatherswaggerui",
                ClientName = "Weather Swagger UI",
                AllowedGrantTypes = GrantTypes.Implicit,
                AllowAccessTokensViaBrowser = true,

                RedirectUris = {"https://localhost:5445/swagger/oauth2-redirect.html"},
                PostLogoutRedirectUris = { "https://localhost:5445/swagger/" },

                AllowedScopes = { "weatherswaggerui.read", "weatherswaggerui.write" },

            }
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("weatherapi", "Weather Service")
            {
                Scopes = new List<string> { "weatherapi.read", "weatherapi.write" },
                ApiSecrets = new List<Secret> { new Secret("ScopeSecret".Sha256()) },
                UserClaims = new List<string> { "role" }
            },
            new ApiResource("weatherswaggerui", "Weather Swagger UI")
            {
                Scopes = new List<string> { "weatherswaggerui.read", "weatherswaggerui.write" }
            }
        };
    }

    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResource
            {
                Name = "role",
                UserClaims = new List<string> { "role" }
            },
        };
    }

    public static IEnumerable<ApiScope> GetApiScopes()
    {
        return new List<ApiScope>
        {
            // weather API specific scopes
            new ApiScope("weatherapi.read"),
            new ApiScope("weatherapi.write"),

            // SWAGGER TEST weather API specific scopes
            new ApiScope("weatherswaggerui.read"),
            new ApiScope("weatherswaggerui.write")
        };
    }
}

下一个项目就是使用 vs2019

创建 Web api 项目时的标准天气 api

WeatherAPI Project startup.cs(注意我创建了在 eShopOnContainers 中找到的扩展方法,因为我喜欢这个流程):

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddCustomAuthentication(Configuration)
                .AddSwagger(Configuration);

    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();

            app.UseSwagger()
                .UseSwaggerUI(options =>
                {
                    options.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather.API V1");
                    options.OAuthClientId("weatherswaggerui");
                    options.OAuthAppName("Weather Swagger UI");
                });
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

public static class CustomExtensionMethods
{
    public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "Find Scrims - Weather HTTP API Test",
                Version = "v1",
                Description = "Randomly generates weather data for API testing"
            });
            options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.OAuth2,
                Flows = new OpenApiOAuthFlows()
                {
                    Implicit = new OpenApiOAuthFlow()
                    {
                        AuthorizationUrl = new Uri($"{ configuration.GetValue<string>("IdentityUrl")}/connect/authorize"),
                        TokenUrl = new Uri($"{ configuration.GetValue<string>("IdentityUrl")}/connect/token"),
                        Scopes = new Dictionary<string, string>()
                        {
                            { "weatherswaggerui", "Weather Swagger UI" }
                        },
                    }
                }
            });

            options.OperationFilter<AuthorizeCheckOperationFilter>();
        });

        return services;
    }

    public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
    {
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");

        var identityUrl = configuration.GetValue<string>("IdentityUrl");

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.Authority = identityUrl;
            options.RequireHttpsMetadata = false;
            options.Audience = "weatherapi";
        });

        return services;
    }
}

最后是AuthorizeCheckOperationFilter.cs

public class AuthorizeCheckOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
                           context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();

        if (!hasAuthorize) return;

        operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
        operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });

        var oAuthScheme = new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference { 
                Type = ReferenceType.SecurityScheme, 
                Id = "oauth2" 
            }
        };

        operation.Security = new List<OpenApiSecurityRequirement>
        {
            new OpenApiSecurityRequirement
            {
                [oAuthScheme ] = new [] { "weatherswaggerui" }
            }
        };
    }
}

再次强调,任何帮助或关于指南的建议将不胜感激,因为 google 没有为我提供解决此问题的任何结果。我对 IdentityServer4 很陌生,我假设它是一个小问题,因为客户端和 ApiResources 和 ApiScopes。谢谢。

swagger 客户端需要访问 api,为此它需要 api 范围。你拥有的 swagger 范围并没有这样做。更改 swagger 客户端“weatherswaggerui”的范围以包含 api 范围,如下所示:

AllowedScopes = {"weatherapi.read"}