.NET Core Api 使用身份服务器 4 进行身份验证

.NET Core Api authentication using Identity server 4

我正在尝试使用带有身份服务器 4 的 .net 核心 API 3.1 创建一个小型电子商务演示应用程序。


Config.cs(Demo.Auth 项目)

    public static class Config
    {
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            {                
                new IdentityResources.Profile(),
            };
        public static IEnumerable<ApiResource> ApiResources => new[]
        {
            new ApiResource("Demo.Api", "Demo Api")
        };

        public static IEnumerable<Client> Clients => new[]
        {
            new Client()
            {
                ClientId = "mvc",
                ClientName = "Demo.MvcClient",
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                RequirePkce = true,
                ClientSecrets =
                {
                    new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256())
                },
                RedirectUris = {"http://localhost:5003/signin-oidc"},
                FrontChannelLogoutUri = "http://localhost:5003/signout-oidc",
                PostLogoutRedirectUris = {"http://localhost:5003/signout-callback-oidc"},

                AllowOfflineAccess = true,
                AllowedScopes = {"profile"}
            }
        };
    }


Startup.cs(Demo.Auth 项目)

    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            IConfigurationRoot config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build();

            string identityConnectionString = config.GetSection("ConnectionStrings")
                .Value;
            var migratingAssembly = typeof(Startup).GetTypeInfo()
                .Assembly.GetName()
                .Name;

            if (config.GetValue<bool>("UseInMemoryDatabase"))
            {
                services.AddIdentityServer(options =>
                    {
                        options.Events.RaiseErrorEvents = true;
                        options.Events.RaiseInformationEvents = true;
                        options.Events.RaiseFailureEvents = true;
                        options.Events.RaiseSuccessEvents = true;
                    })
                    .AddTestUsers(TestUsers.Users)
                    .AddInMemoryIdentityResources(Config.Ids)
                    .AddInMemoryApiResources(Config.ApiResources)
                    .AddInMemoryClients(Config.Clients)
                    .AddDeveloperSigningCredential();
            }
            else
            {
                services.AddIdentityServer(options =>
                    {
                        options.Events.RaiseErrorEvents = true;
                        options.Events.RaiseInformationEvents = true;
                        options.Events.RaiseFailureEvents = true;
                        options.Events.RaiseSuccessEvents = true;
                    })
                    .AddTestUsers(TestUsers.Users)
                    .AddDeveloperSigningCredential()
                    //This will store client and ApiResource
                    .AddConfigurationStore(options =>
                    {
                        options.ConfigureDbContext = b => b.UseSqlServer(identityConnectionString,
                            sql => sql.MigrationsAssembly(migratingAssembly));
                    })
                    //This will store token, consent or code
                    .AddOperationalStore(options =>
                    {
                        options.ConfigureDbContext = b => b.UseSqlServer(identityConnectionString,
                            sql => sql.MigrationsAssembly(migratingAssembly));
                    });
            }
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app,
            IWebHostEnvironment env)
        {
            // this will do the initial DB population
           // InitializeDatabase(app);

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

            app.UseRouting();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/",
                    async context => { await context.Response.WriteAsync("Hello World!"); });
            });
        }       
    }


Startup.cs(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)
        {
            services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ApiName = "Demo.Api";
            });

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

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

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


WeatherForecastController(属于 Demo.Api 项目)

    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;
        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }


我在 postman 中测试了 API,它工作正常。 "Demo.Auth" 项目正在生成令牌,我能够成功访问我的授权控制器。

这里的想法是:

MVC 客户端 ----> 身份服务器项目 ---> API

MVC 客户端想要访问 API。所以我将在身份服务器项目上验证 Mvc 客户端,如果他是有效用户则生成令牌,然后我将调用我的 api。

注意:目前我使用的是MVC客户端,但稍后我会添加一个客户端,可能是Angular。

但是我有一个问题。
如何将用户添加到我的数据库并验证数据库用户而不是测试用户。
我不明白的另一件事是我应该在哪里放置登录和注册功能以及该代码的外观。

我是身份服务器的新手,请原谅。

有人可以用一些代码指导我解决上述问题吗?提前致谢

创建和维护用户的责任是身份验证服务器。

Where I should put the Login and Register functionality

因此,Identity Server 项目将包含注册、登录、忘记密码等端点。

How can I add the users to my database and authenticate the database user not the Test one.

Microsoft 身份核心

您可以实施 Microsoft Identity Core,它提供与帐户管理相关的所有功能。 并且在 IdentityServer4 中内置了对它的支持。

这样您就不必担心代码或数据库。

Note: The Microsoft Identity Core does a lot of things under the hood so you won't be able to understand how actually it works with IdentityServer4.

您可以从here (Open Startup.cs) and documentation from here找到示例代码。

您还可以通过行编码查看 this YouTube 系列。

自定义用户存储库

如果您想在不使用 Microsoft Identity Core 的情况下验证用户,那么您可以实现 IResourceOwnerPasswordValidator 接口,可以在此处找到示例代码 here and blog from here

在 ResourceOwnerPasswrod 流程中,您可以在客户端保留注册和登录功能,并且可以根据数据库用户验证用户。

您应该实施自定义用户存储以验证用户并从数据库添加声明。像下面这样更改启动代码,Userrepository class 代表数据库通信以验证用户并从数据库获取声明:

更新身份配置的启动配置方法:

var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
.AddCustomUserStore();

添加以下 classes 并根据您的要求进行更改:

public static class CustomIdentityServerBuilderExtensions
{
    public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
    {                   
        builder.AddProfileService<UserProfileService>();           
        builder.AddResourceOwnerValidator<UserResourceOwnerPasswordValidator>();
        return builder;
    }
}

public class UserProfileService : IProfileService
{
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
            UserRepository userRepository=new UserRepository();
            var user = userRepository.GetUserById(int.Parse(context.Subject.GetSubjectId()));
            if (user != null)
            {
                var userTokenModel = _mapper.Map<UserTokenModel>(user);
                var claims = new List<Claim>();
                claims.Add(new Claim("UserId", user.UserId));
                // Add another claims here 
                context.IssuedClaims.AddRange(claims);                    
    }
    public async Task IsActiveAsync(IsActiveContext context)
    {          
    }
}

public class UserResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{        
    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {           
            UserRepository userRepository=new UserRepository();
            var userLoginStatus = userRepository.GetUserById(context.UserName, context.Password);

            if (userLoginStatus != null)
            {

                    context.Result = new GrantValidationResult(userLoginStatus.UserId.ToString(),
                         OidcConstants.AuthenticationMethods.Password);                   
            }
            else
            {                    
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient, 
                        "Wrong Credentials");
            }            
    }
}

参考 ASP.NET CORE IDENTITYSERVER4 RESOURCE OWNER PASSWORD FLOW WITH CUSTOM USERREPOSITORY 了解 ResourceOwnerPasswrod 流程。建议使用此流程来支持旧应用程序。

还有更多流:

  1. 隐式
  2. 混合
  3. 授权码

参考official documentation了解更多详情。

How can I add the users to my database and authenticate the database user not the Test one.

Another thing which I am not understanding is where I should put the Login and Register functionality and how that code will look like.

有一种方法可以使用主要来自 IdentityServer4 Quickstarts 的实现来创建符合您要求的工作示例。

步骤是(使用SQL数据库):

  1. 使用 dotnet is4aspid 模板创建 mvc 核心项目。它将 IdentityServer 配置为项目的中间件,您可以通过准备好迁移更新数据库,为 IdentityServer 的 ASP.NET 核心身份和登录、注销、同意、授权 (UI) 功能创建所有表。 (在数据库更新之前的 CreaeteIdentitySchema.cs 文件中,将标识列的注释替换为符合 SQL 数据库,如: Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn) ,模板中的模式用于 SQL站点数据库)

  2. 在mvc核心项目Startup.cs中激活Razore Pages,添加services.AddRazorPages()和endpoints.MapRazorPages(),就可以添加脚手架和您可以使用 Razor Class 库添加注册和维护用户帐户(本地和外部提供商)所需的所有页面。出于身份验证目的,登录和注销页面应由 IdentityServer 控制。

  3. 接下来,您可以使用 IdentityServer4.EntityFramework.Storage nuget 包中的 ConfigurationDbContext 、 PersistedGrantDbContext 上下文及其实体来创建迁移并更新现有的 ASP.NET 身份数据库,为客户端、资源添加表,和范围,也用于临时操作数据,例如授权代码和刷新令牌。要向这些表添加、删除或更新数据,您可以使用这两个上下文手动创建界面。

  4. 最后一步是根据 Quickstarts 创建客户端和 Api 项目,并使用 IdentityServer 配置它们。

Startup.cs 文件最后将是:

    public class Startup
{
    public IWebHostEnvironment Environment { get; }
    public IConfiguration Configuration { get; }

    public Startup(IWebHostEnvironment environment, IConfiguration configuration)
    {
        Environment = environment;
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IEmailSender, EmailSender>();

        services.AddControllersWithViews();

        services.Configure<IISOptions>(iis =>
        {
            iis.AuthenticationDisplayName = "Windows";
            iis.AutomaticAuthentication = false;
        });

        services.Configure<IISServerOptions>(iis =>
        {
            iis.AuthenticationDisplayName = "Windows";
            iis.AutomaticAuthentication = false;
        });

        var migrationsAssembly =    typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
        
        var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
            })
            .AddConfigurationStore(options =>
            {
                options.ConfigureDbContext = b =>   b.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
            })
            .AddOperationalStore(options =>
            {
                options.ConfigureDbContext = b =>           b.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
            })
            .AddAspNetIdentity<ApplicationUser>();

        builder.AddDeveloperSigningCredential();

        services.AddRazorPages();

        services.AddAuthentication()
            .AddGoogle(options =>
            {
                options.ClientId = "copy client ID from Google here";
                options.ClientSecret = "copy client secret from Google here";
            });
    }

    public void Configure(IApplicationBuilder app)
    {
        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }

        app.UseStaticFiles();

        app.UseRouting();
        app.UseIdentityServer();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapRazorPages();
        });
    }

}