授权属性不适用于 IdentityServer4 和 .NET Core 3.1

Authorize attribute not working with IdentityServer4 and .NET Core 3.1

我有一个 .NET Core 3.1 项目使用 Identity 和 IdentityServer4 来实现资源所有者密码授权类型。我可以毫无问题地获得令牌,但 [Authorize] 属性不起作用,它只是让一切通过。重要的一点是我的 API 和身份服务器在同一个项目中。从网上的评论来看,这似乎是一个中间件顺序问题,但我似乎找不到有效的组合。我已经仔细检查过,当没有附加授权 header 时,端点代码仍然被命中。

这是我的 Startup.cs 文件:

using System;
using System.Collections.Generic;
using IdentityServer4.Models;
using LaunchpadSept2020.App;
using LaunchpadSept2020.App.Repositories;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.App.Seeds;
using LaunchpadSept2020.Models.Entities;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace LaunchpadSept2020.Api
{
    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)
        {
            // Set up the database
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
                b =>
                {
                    b.MigrationsAssembly("LaunchpadSept2020.App");
                })
            );

            services.AddIdentity<User, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.Configure<IdentityOptions>(options =>
            {
                options.Password.RequiredLength = 6;
                options.Password.RequireLowercase = true;
                options.Password.RequireUppercase = true;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireDigit = true;
            });

            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.ApiName = "launchpadapi";
                    options.Authority = "http://localhost:25000";
                    options.RequireHttpsMetadata = false;
                });

            services.AddIdentityServer()
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder => builder.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
                        npgSqlOptions =>
                        {
                            npgSqlOptions.MigrationsAssembly("LaunchpadSept2020.App");
                        });
                })
                .AddInMemoryClients(Clients.Get())
                .AddAspNetIdentity<User>()
                .AddInMemoryIdentityResources(Resources.GetIdentityResources())
                .AddInMemoryApiResources(Resources.GetApiResources())
                .AddInMemoryApiScopes(Resources.GetApiScopes())
                .AddDeveloperSigningCredential();

            services.AddControllers();

            // Add Repositories to dependency injection
            services.AddScoped<ICompanyRepository, CompanyRepository>();
            services.AddScoped<IUserRepository, UserRepository>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserManager<User> userManager, RoleManager<IdentityRole> roleManager)
        {
            // Initialize the database
            UpdateDatabase(app);

            // Seed data
            UserAndRoleSeeder.SeedUsersAndRoles(roleManager, userManager);

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //app.UseHttpsRedirection();
            app.UseRouting();

            app.UseIdentityServer(); // Includes UseAuthentication
            app.UseAuthorization();

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

        // Update the database to the latest migrations
        private static void UpdateDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices
                 .GetRequiredService<IServiceScopeFactory>()
                 .CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>())
                {
                    context.Database.Migrate();
                }
            }
        }
    }

    internal class Clients
    {
        public static IEnumerable<Client> Get()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "mobile",
                    ClientName = "Mobile Client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    ClientSecrets = { new Secret("MySecret".Sha256()) },
                    AllowedScopes = new List<String> { "launchpadapi.read" }
                    //AllowAccessTokensViaBrowser = true,
                    //RedirectUris = { "http://localhost:25000/signin-oidc" },
                    //PostLogoutRedirectUris = { "http://localhost:25000/signout-callback-oidc" },
                    //AllowOfflineAccess = true
                }
            };
        }
    }

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

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new[]
            {
            new ApiResource
            {
                Name = "launchpadapi",
                DisplayName = "Launchpad API",
                Description = "Allow the application to access the Launchpad API on your behalf",
                Scopes = new List<string> { "launchpadapi.read", "launchpadapi.write"},
                ApiSecrets = new List<Secret> {new Secret("ScopeSecret".Sha256())},
                UserClaims = new List<string> {"role"}
            }
        };
        }

        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new[]
            {
                new ApiScope("launchpadapi.read", "Read Access to Launchpad API"),
                new ApiScope("launchpadapi.write", "Write Access to Launchpad API")
            };
        }
    }
}

还有我的控制器:

using System.Collections.Generic;
using System.Threading.Tasks;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.Models.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace LaunchpadSept2020.Api.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CompanyController : ControllerBase
    {
        private readonly ICompanyRepository _companyRepository;

        public CompanyController(ICompanyRepository companyRepository)
        {
            _companyRepository = companyRepository;
        }

        [HttpPost]
        [Authorize]
        public async Task<ActionResult<CompanyVM>> Create([FromBody] CompanyCreateVM data)
        {
            // Make sure model has all required fields
            if (!ModelState.IsValid)
                return BadRequest("Invalid data");

            try
            {
                var result = await _companyRepository.Create(data);
                return Ok(result);
            }
            catch
            {
                return StatusCode(500);
            }
        }

        [HttpGet]
        [Authorize]
        public async Task<ActionResult<List<CompanyVM>>> GetAll()
        {
            try
            {
                var results = await _companyRepository.GetAll();
                return Ok(results);
            }
            catch
            {
                return StatusCode(500);
            }
        }
    }
}

我认为一个普遍的问题是您将 IdentityServer 与 ASP.NET 身份混合在同一个应用程序中,总的来说,我的经验是很难知道谁在做什么,也很难完全理解。我总是建议将 IdentityServer 和 API 放在独立的服务中。只是为了清楚地分离关注点。

对于本地 API 身份验证,您需要在启动中进行以下额外配置:

public void ConfigureServices(IServiceCollection services)
{
  ....
  // After services.AddIdentityServer()
  services.AddLocalApiAuthentication();
}

参考 docs

然后您需要将本地 API 政策指定为 API 上 Authorize 属性的一部分:

[Authorize(LocalApi.PolicyName)]

见当地人 API example.