使用.NET Core Identity与API

9
我已经创建了一个API,并从该API设置了JWT身份验证(我选择不使用IdentityServer4)。
我是通过services.AddAuthentication进行操作的。 然后我在控制器中创建了令牌,它可以正常工作。
但是,我现在想添加注册等功能,但我不想为散列密码、处理注册电子邮件等编写自己的代码。
所以我发现了ASP.NET Core Identity,它似乎是我需要的,除了它添加了一些我不需要的UI功能(因为它只是一个API,我需要完全独立的UI)。
但在MSDN上写道:
ASP.NET Core Identity向ASP.NET Core Web应用程序添加用户界面(UI)登录功能。 要保护Web API和SPAS,请使用以下之一:
Azure Active Directory
Azure Active Directory B2C(Azure AD B2C)
IdentityServer4
那么,仅将Core Identity用于API的哈希和注册逻辑真的很糟糕吗? 我不能忽略UI功能吗? 这非常令人困惑,因为我宁愿不使用IdentityServer4或创建自己的用户管理逻辑。

1
该软件包中有一些服务,您可以直接注册和注入。 - Aluan Haddad
你所说的UI可能只是可以生成的模板,没有必要使用它。正如Aluan所提到的,Identity包含了一些服务(UserManagerSignInManager等),你可以使用它们。 - Xerillio
1
@Xerillio 如果我通过控制器提供JWT令牌而不是通过身份验证cookie,那么SignInManager是否仍然有效?它不是期望cookie吗?还是它会适应JWT。 - Jonathan Daniel
2个回答

13
让我先说一下,Identity与UI、Cookies及令人困惑的各种扩展方法捆绑在一起,有些烦人,至少在构建不需要Cookies或UI的现代Web API时如此。
在某些项目中,我还使用Identity手动生成JWT令牌来进行成员资格功能和用户/密码管理。
基本上,最简单的方法是查看源代码。
1. AddDefaultIdentity()添加身份验证、添加Identity cookies、添加UI,并调用AddIdentityCore();但不支持角色:
public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
    services.AddAuthentication(o =>
    {
        o.DefaultScheme = IdentityConstants.ApplicationScheme;
        o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies(o => { });

    return services.AddIdentityCore<TUser>(o =>
    {
        o.Stores.MaxLengthForKeys = 128;
        configureOptions?.Invoke(o);
    })
        .AddDefaultUI()
        .AddDefaultTokenProviders();
}
  1. AddIdentityCore() 是一个更加简化版本的方法,它仅添加基本服务,但不包括身份验证和角色支持(在这里可以看到已添加的个别服务,如果需要可以更改/覆盖/删除它们):

public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
    where TUser : class
{
    // Services identity depends on
    services.AddOptions().AddLogging();

    // Services used by identity
    services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
    services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
    services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
    services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
    services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
    // No interface for the error describer so we can add errors without rev'ing the interface
    services.TryAddScoped<IdentityErrorDescriber>();
    services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
    services.TryAddScoped<UserManager<TUser>>();

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

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

目前为止,这种解释还算讲得通,对吧?

  1. 但是现在加入AddIdentity()之后,它似乎是最臃肿的一个,唯一直接支持角色的方法,但令人困惑的是它似乎没有添加UI界面:
public static IdentityBuilder AddIdentity<TUser, TRole>(
    this IServiceCollection services,
    Action<IdentityOptions> setupAction)
    where TUser : class
    where TRole : class
{
    // Services used by identity
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
        options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddCookie(IdentityConstants.ApplicationScheme, o =>
    {
        o.LoginPath = new PathString("/Account/Login");
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
        };
    })
    .AddCookie(IdentityConstants.ExternalScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.ExternalScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    })
    .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
        };
    })
    .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    });

    // 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<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
    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);
}

总的来说,你可能需要使用 AddIdentityCore(),并自己使用 AddAuthentication()。此外,如果您使用了AddIdentity(),请确保在调用AddIdentity()之后运行AddAuthentication()配置,因为您必须覆盖默认的身份验证方案(我遇到了与此相关的问题,但是无法记住细节)。还有一些关于授权的信息可能对阅读此内容的人很有趣,比如SignInManager.PasswordSignInAsync()SignInManager.CheckPasswordSignInAsync()UserManager.CheckPasswordAsync() 之间的区别。这些都是公共方法,您可以找到并调用它们进行授权。 PasswordSignInAsync() 实现了双因素登录(还设置 cookie;可能仅在使用AddIdentity()AddDefaultIdentity()时才使用)并调用 CheckPasswordSignInAsync(),该方法实现了用户锁定处理并调用 UserManager.CheckPasswordAsync(),该方法仅检查密码。因此,为了获得适当的身份验证,最好不要直接调用UserManager.CheckPasswordAsync(),而是通过 CheckPasswordSignInAsync() 进行调用。但是,在单因素 JWT 令牌方案中,调用 PasswordSignInAsync() 可能是不需要的(并且可能会遇到重定向问题)。如果你在Startup中包含了 UseAuthentication()/AddAuthentication() 并设置了适当的 JwtBearer 令牌方案,则客户端下一次附带有效令牌发送请求时,身份验证中间件将启动,客户端将被“登录”;即任何有效的 JWT 令牌都将允许客户端访问受 [Authorize] 保护的控制器操作。值得庆幸的是,IdentityServer 完全与 Identity 分离。实际上,IdentityServer 的不错实现是将其用作独立的文字身份服务器,为您的服务发放令牌。但是,由于 ASP.NET Core 没有内置的令牌生成功能,因此很多人最终在其应用程序中运行这个臃肿的服务器,只是为了能够使用 JWT 令牌,尽管他们只有一个应用程序并且没有实际用途的中央权威机构。我的意思不是贬低它,它是一个拥有很多特性的真正伟大的解决方案,但对于更常见的用例,拥有一些更简单的东西会很好。

1
谢谢,这解决了我的问题。我可能会创建一个新的方法,只添加我需要的内容。 - Jonathan Daniel
2
这是一个好主意;值得花些时间尝试各种组件,因为这会让你意识到在简单场景下你不需要太多东西,而且它将简化你对认证过程的思考。"登录"特别臃肿,而对于简单的身份验证,你只需要在请求中存在有效的令牌(前提是你添加了正确方案的身份验证中间件)。 - Leaky
1
谢谢你的回答,真的很有帮助,可以帮助理清Identity中发生了什么。我还搭建了Razor页面,以便从客户端角度查看它们的代码,这也有所帮助。同意身份验证的捆绑确实很烦人。就像你所说的,如果你想要一个独立的OpenID授权服务器,那它是很好的,但对于许多应用程序来说似乎过度了。 - David Brunning

5
你只需要配置 Identity 使用 JWT bearer token。在我的情况下,我使用了加密的 token,因此根据你的用例,你可能需要调整配置:
// In Startup.ConfigureServices...
services.AddDefaultIdentity<ApplicationUser>(
    options =>
    {
        // Configure password options etc.
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Configure authentication
services.AddAuthentication(
    opt =>
    {
        opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
            options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            TokenDecryptionKey =
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key")),
            RequireSignedTokens = false, // False because I'm encrypting the token instead
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });


// Down in Startup.Configure add authn+authz middlewares
app.UseAuthentication();
app.UseAuthorization();

当用户想要登录时,请生成一个令牌:

var encKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key"));
var encCreds = new EncryptingCredentials(encKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);

var claimsIdentity = await _claimsIdentiyFactory.CreateAsync(user);

var desc = new SecurityTokenDescriptor
{
    Subject = claimsIdentity,
    Expires = DateTime.UtcNow.AddMinutes(_configuration.Identity.JwtExpirationMinutes),
    Issuer = _configuration.Identity.JwtIssuer,
    Audience = _configuration.Identity.JwtAudience,
    EncryptingCredentials = encCreds
};

var token = new JwtSecurityTokenHandler().CreateEncodedJwt(desc);
// Return it to the user

此后您可以使用UserManager来处理创建新用户和检索用户,而SignInManager可用于在生成令牌之前检查有效的登录/凭据。


哇,谢谢,我会尝试的。MSDN上的引用太令人困惑了。 - Jonathan Daniel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接