我创建了一个 API 并从同一 API 设置了 JWT 身份验证(我选择不使用 IdentityServer4)。
我通过
services.AddAuthentication
做到了这一点
然后我在控制器中创建了令牌并且它起作用了。
但是我现在想添加注册等。但我不想编写自己的代码来哈希密码、处理注册电子邮件等。
所以我遇到了 ASP.NET Core Identity,它似乎是我需要的,除了它添加了一些我不需要的 UI 东西(因为它只是一个 API 和我想要完全独立的 UI)。
但是MSDN上写着:
ASP.NET Core Identity 添加了用户界面 (UI) 登录功能 ASP.NET Core Web 应用程序。要保护 Web API 和 SPA,请使用其中之一 以下:
Azure 活动目录
Azure Active Directory B2C(Azure AD B2C)
身份服务器4
那么仅仅使用 Core Identity 来进行 API 的哈希和注册逻辑真的是一个坏主意吗?我不能忽略 UI 功能吗?这非常令人困惑,因为我不想使用 IdentityServer4 或创建自己的用户管理逻辑。
让我发泄一下,捆绑身份与 UI、cookie 以及令人困惑的各种扩展方法(添加这个或那个,但不要添加这个或那个)是相当烦人的,至少当你构建现代的时候不需要 cookie 或 UI 的 Web API。
在一些项目中,我还使用手动 JWT 令牌生成和身份来实现会员功能和用户/密码管理。
基本上最简单的事情就是检查源代码。
AddDefaultIdentity()
添加身份验证,添加身份cookie,添加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();
}
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);
}
到目前为止,这是有道理的,对吧?
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()
,请务必在调用AddAuthentication()
之后运行AddIdentity()
配置,因为您必须覆盖默认的身份验证方案(我遇到了与此相关的问题,但不记得了)详细信息)。
(阅读本文的人可能会感兴趣的另一个信息是
SignInManager.PasswordSignInAsync()
、SignInManager.CheckPasswordSignInAsync()
和 UserManager.CheckPasswordAsync()
之间的区别。这些都是您可以出于授权目的找到和调用的公共方法。PasswordSignInAsync()
实现两个 -因素登录(也设置cookie;可能仅在使用AddIdentity()
或AddDefaultIdentity()
时)并调用CheckPasswordSignInAsync()
,它实现用户锁定处理并调用UserManager.CheckPasswordAsync()
,它仅检查密码。因此,为了获得正确的身份验证,最好不要直接调用 UserManager.CheckPasswordAsync()
,而是通过 CheckPasswordSignInAsync()
来完成。但是,在单因素 JWT 令牌场景中,可能不需要调用 PasswordSignInAsync()
(并且可能会遇到重定向问题)。如果您已包含UseAuthentication()/AddAuthentication()
在启动中设置了正确的 JwtBearer 令牌方案,那么下次客户端发送附加了有效令牌的请求时,身份验证中间件将启动,客户端将“登录”;即任何有效的 JWT令牌将允许客户端访问受[授权]保护的控制器操作。)
值得庆幸的是,IdentityServer 与 Identity 完全分开。事实上,IdentityServer 的正确实现是将其用作独立的文字身份服务器,为您的服务颁发令牌。但由于 ASP.NET Core 没有内置的令牌生成功能,许多人最终在他们的应用程序中运行这个臃肿的服务器只是为了能够使用 JWT 令牌,即使他们只有一个应用程序并且没有实际用途对于一个中央机关来说。我并不是要讨厌它,它是一个非常好的解决方案,具有很多功能,但对于更常见的用例来说,如果有一些更简单的东西那就太好了。
您只需配置 Identity 即可使用 JWT 不记名令牌。就我而言,我使用加密令牌,因此根据您的用例,您可能需要调整配置:
// 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
可用于在生成令牌之前检查有效的登录/凭据。
自 .NET 8 以来,.NET core api 的身份验证和授权得到了简化和改进。您现在可以使用 Identity 包中的 AddIdentityApiEndpoints