.Net Core 2.0 Web API 使用 JWT - 添加身份会破坏 JWT 身份验证

.Net Core 2.0 Web API using JWT - Adding Identity breaks the JWT authentication

(编辑 - 找到正确的修复!见下文)

好的 - 这是我第一次尝试 .Net Core 2.0 和身份验证,尽管我过去曾使用 Web API 2.0 做过一些事情,并且在各种 MVC 和 Webforms 上进行了相当广泛的工作 ASP 过去几年的项目。

我正在尝试使用 .Net Core 创建 Web API ONLY 项目。这将构成用于生成某些报告的多租户应用程序的后端,因此我需要能够对用户进行身份验证。似乎通常的方法是使用 JWT - 首先对用户进行身份验证以生成令牌,然后将其传递给客户端以在每个 API 请求上使用。将使用 EF Core 存储和检索数据。

我遵循了 this post 的基本方法来设置此设置,并且我设法让它正常工作 - 我有一个接受 username/password 和 returns 的控制器令牌(如果有效)和一些基于声明设置的授权策略。

接下来我需要做的是实际管理 users/passwords/etc。我以为我会为此使用 .Net Core Identity,因为那样我会有很多现成的代码来担心 users/roles、密码等。我使用的是自定义 User class 和 UserRole classes 派生自标准的 IdentityUserIdentityRole classes,但我现在已经恢复到标准的。

我遇到的问题是我无法完全弄清楚如何在不破坏身份验证的情况下添加身份并注册所有各种服务(rolemanager、usermanager 等)——基本上只要我将此行添加到我的 Startup.ConfigureServices class:

services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<MyContext>();

一切都出错了,当我收到请求时我再也看不到任何索赔,所以所有的政策都被锁定了,你什么也做不了。

如果我没有这些行,那么我最终会遇到与 UserManager、RoleManager、UserStore 等相关的错误,所有这些都没有为 DI 注册。

那么...我如何(如果可能的话)注册身份并将其正确连接到上下文,但是 avoid/Remove 对实际授权机制有任何更改吗?

我在网上浏览了很多,但是自从 .Net Core 1.x 以来,很多内容都发生了变化,所以很多教程等都不再有效了。

我不打算让这个 API 应用程序有任何前端代码,所以我现在不需要对表单或任何东西进行任何 cookie 身份验证。

编辑
好的,我现在发现在这段代码中,在 Startup.ConfigureServices() 方法中设置了 JWT 身份验证:

 services.AddAuthentication(
            JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                 >>breakpoint>>>   options.TokenValidationParameters =
                        new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateLifetime = true,
                            ValidateIssuerSigningKey = true,

                            ValidIssuer = "Blah.Blah.Bearer",
                            ValidAudience = "Blah.Blah.Bearer",
                            IssuerSigningKey =
                            JwtSecurityKey.Create("verylongsecretkey")

                        };
                });

如果我在指示的行放置一个断点(通过“>>断点>>>”)然后当我添加行以添加身份时它会被击中服务,但如果我确实添加了这些行,那么它 永远不会 被击中。无论我将 services.AddIdentity() 调用放在方法的哪个位置,都是如此。我知道这只是一个 lambda,所以它会在稍后执行,但是有什么方法可以让 AddIdentity 东西不设置身份验证,或者让代码立即删除它?我假设在某些时候有一些代码选择不 运行 Lambda for config I've set there as Identity stuff had already set it...

感谢您阅读所有内容,如果您有:)

编辑 - 找到答案
好的,我最终发现了这个 GH 问题,基本上就是这个问题: https://github.com/aspnet/Identity/issues/1376

基本上我必须做的是双重的:

确保首先

调用了services.AddIdentity<IdentityUser, IdentityContext()

从以下位置更改添加身份验证的调用:

services.AddAuthentication(
            JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
...

收件人:

services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddJwtBearer(options =>
...

这确实令人恼火地导致创建了一个 cookie,但据我所知,这并没有用于身份验证 - 它纯粹是在 controllers/actions 的请求上使用持有者令牌 [Authorize(Policy = "Administrator")] 或类似设置至少。

我需要进行更多测试,如果我发现它在某些方面不起作用,我会尝试返回这里进行更新。

(已编辑 - 现在将正确的解决方案作为答案)

我最终整理了解决方案,所以根据用户 alwayslearning 的建议,我编辑了我的 post 并将其作为实际答案放入。

ok,这个可以妥善处理。首先,您需要使用我在上面的编辑中指出的身份验证选项 - 这很好。 那么你需要使用services.AddIdentityCore<TUser>()而不是services.AddIdentity<TUser>()。然而,这并没有为角色管理添加一大堆东西,而且显然缺少适当的构造函数来为其提供您想要使用的角色类型。这意味着在我的情况下我必须这样做:

  IdentityBuilder builder = services.AddIdentityCore<IdentityUser>(opt =>
        {
            opt.Password.RequireDigit = true;
            opt.Password.RequiredLength = 8;
            opt.Password.RequireNonAlphanumeric = false;
            opt.Password.RequireUppercase = true;
            opt.Password.RequireLowercase = true;
        }
        );
        builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
        builder
            .AddEntityFrameworkStores<MyContext>();
        //.AddDefaultTokenProviders();

        builder.AddRoleValidator<RoleValidator<IdentityRole>>();
        builder.AddRoleManager<RoleManager<IdentityRole>>();
        builder.AddSignInManager<SignInManager<IdentityUser>>();

完成后,下一步是确保在验证用户登录时(在发送令牌之前),确保使用 SignInManager 方法 CheckPasswordSignInAsync PasswordSignInAsync:

public async Task<IdentityUser> GetUserForLogin(string userName, string password)
    {   
        //find user first...
        var user = await _userManager.FindByNameAsync(userName);

        if (user == null)
        {
            return null;
        }

        //validate password...
        var signInResult = await _signInManager.CheckPasswordSignInAsync(user, password, false);

        //if password was ok, return this user.
        if (signInResult.Succeeded)
        {
            return user;
        }

        return null;
    }

如果您使用 PasswordSignInAsync 方法,那么您将收到运行时错误。没有配置 IAuthenticationSignInHandler。

我希望这对某些人有所帮助。

我已经从 github 中提取了 AddIdentity 代码并基于它创建了一个扩展方法,它不添加默认的 Cookie 验证器,它现在与内置的 [=13] 非常相似=] 但可以接受 IdentityRole.

 /// <summary>
 /// Contains extension methods to <see cref="IServiceCollection"/> for configuring identity services.
 /// </summary>
 public static class IdentityServiceExtensions
 {
     /// <summary>
     /// Adds the default identity system configuration for the specified User and Role types. (Without Authentication Scheme)
     /// </summary>
     /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
     /// <typeparam name="TRole">The type representing a Role in the system.</typeparam>
     /// <param name="services">The services available in the application.</param>
     /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
     public static IdentityBuilder AddIdentityWithoutAuthenticator<TUser, TRole>(this IServiceCollection services)
         where TUser : class
         where TRole : class
         => services.AddIdentityWithoutAuthenticator<TUser, TRole>(setupAction: null);

     /// <summary>
     /// Adds and configures the identity system for the specified User and Role types. (Without Authentication Scheme)
     /// </summary>
     /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
     /// <typeparam name="TRole">The type representing a Role in the system.</typeparam>
     /// <param name="services">The services available in the application.</param>
     /// <param name="setupAction">An action to configure the <see cref="IdentityOptions"/>.</param>
     /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
     public static IdentityBuilder AddIdentityWithoutAuthenticator<TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction)
         where TUser : class
         where TRole : class
     {
         // Hosting doesn't add IHttpContextAccessor by default
         services.AddHttpContextAccessor();
         // Identity services
         services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
         services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
         services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
         services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
         services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
         // No interface for the error describer so we can add errors without rev'ing the interface
         services.TryAddScoped<IdentityErrorDescriber>();
         services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
         services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
         services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
         services.TryAddScoped<UserManager<TUser>>();
         services.TryAddScoped<SignInManager<TUser>>();
         services.TryAddScoped<RoleManager<TRole>>();

         if (setupAction != null)
         {
             services.Configure(setupAction);
         }

         return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
     }
 }

现在你可以像这样在WebApi项目中正常使用上面的代码了

.AddIdentityWithoutAuthenticator<User, IdentityRole>()