- 基于IdentityServer4的统一身份认证中心
- IdentityServer4是什么?能做什么?请参阅官方文档:https://identityserver4docs.readthedocs.io/zh-cn/latest/index.html
- 知识储备
- OAuth2.0
- Client:客户端
- Resource Owner:资源所有者
- Authorization Server:认证服务器,即服务提供商专门用来处理认证的服务器。
- Resource Server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
- 4种授权模式:授权码模式(authorization code)、简化模式(implicit)、密码模式(resource owner assword credentials)、客户端模式(client credentials)
- OIDC
- OpenId Connect = OIDC = Authentication + Authorization + OAuth2.0
- OIDC是一个基于OAuth2协议的身份认证标准协议,解决的问题是用户认证,而不关心授权。
- ID Token
- OIDC对OAuth2最主要的扩展就是提供了ID Token.
- ID Token是一个安全令牌,是一个授权服务器提供的包含用户信息(由一组Cliams构成以及其他辅助的Cliams)的JWT格式的数据结构。
- ID Token的主要构成部分如下(使用OAuth2流程的OIDC)。
iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是一个https的url(不包含querystring和fragment部分)。
sub = Subject Identifier:必须。iss提供的EU的标识,在iss范围内唯一。它会被RP用来标识唯一的用户。最长为255个ASCII个字符。
aud = Audience(s):必须。标识ID Token的受众。必须包含OAuth2的client_id。
exp = Expiration time:必须。过期时间,超过此时间的ID Token会作废不再被验证通过。
iat = Issued At Time:必须。JWT的构建的时间。
auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送AuthN请求的时候携带max_age的参数,则此Claim是必须的。
nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID Token和RP本身的Session信息。
acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。
amr = Authentication Methods References:可选。表示一组认证方法。
azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。
- OAuth 2.0、OpenID、OpenID Connect三者的关系 ?
- User:用户
- IdentityServer4.EntityFrameworknuget包实现了所需的存储和服务,主要使用以下两个DbContexts:
- ConfigurationDbContext - 作用于注册数据,如客户端,资源,scope等等
- PersistedGrantDbContext - 作用于临时操作数据,如授权码,refresh tokens
- 数据迁移
- 下面所有操作都在Web项目中,配置好数据库连接,可先建库,也可不建库;
- 首先,你需要在你的项目中安装以下NuGet包:
dotnet add package Pomelo.EntityFrameworkCore.MySql
dotnet add package IdentityServer4.EntityFramework
- 然后,你需要在你的Startup.cs文件中配置IdentityServer4以使用MySQL和EF Core。你需要在AddIdentityServer方法中添加AddConfigurationStore和AddOperationalStore,并在其中配置MySQL数据库的连接字符串和版本:
public void ConfigureServices(IServiceCollection services)
{
string connectionString = Configuration.GetConnectionString("DefaultConnection");
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseMySql(connectionString,
new MySqlServerVersion(new Version(8, 0, 21)),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseMySql(connectionString,
new MySqlServerVersion(new Version(8, 0, 21)),
sql => sql.MigrationsAssembly(migrationsAssembly));
});
}
- 接下来,你需要创建迁移。在命令行中运行以下命令:
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
- 最后,你需要更新数据库以应用这些迁移。在命令行中运行以下命令:
dotnet ef database update -c ConfigurationDbContext
dotnet ef database update -c PersistedGrantDbContext
- 初始化数据
using (var scope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
scope.ServiceProvider.GetService<PersistedGrantDbContext>().Database.Migrate();
var context = scope.ServiceProvider.GetService<ConfigurationDbContext>();
context.Database.Migrate();
}
- 最后
- 很多时候,统一身份认证中心需要同时满足不同场景下的身份认证需求,如:SaaS系统中常见的场景有:管理端登录、C端用户登录、B端用户登录,而且登录方式多样:微信授权、手机验证码,重要的是这些端的用户通常是不是一个表,甚至不在一个库,如何实现?
- 我们的解决方案:
- 扩展IDS4的Grant,在不同的自定义Grant中注入对应业务微服务的远程调用API(rpc&http),如下:
-
namespace Extensions.SMS { /// <summary> /// 自定义一个授权类型 AdminSMSGrantValidator 实现后端管理员用手机+短信验证码登录 /// </summary> public class AdminSMSGrantValidator : IExtensionGrantValidator { public string GrantType => GrantTypeConstants.AdminSMSGrantType; // 系统用户远程调用API,如果是C端用户,那就注入C端用户对应的 API Service private readonly ISystemAdminRemoteService _systemAdminRemoteService; public AdminSMSGrantValidator(ISystemAdminRemoteService systemAdminRemoteService) { _systemAdminRemoteService = systemAdminRemoteService; } public async Task ValidateAsync(ExtensionGrantValidationContext context) { try { var smsCode = context.Request.Raw.Get("sms_code"); var phoneNumber = context.Request.Raw.Get("phone_number"); if (string.IsNullOrEmpty(smsCode) || string.IsNullOrEmpty(phoneNumber)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant); return; } // 1-验证码有效性验证 if (string.IsNullOrEmpty(phoneNumber)) { context.Result = new GrantValidationResult( TokenRequestErrors.InvalidGrant, "短信验证码有误!"); } // 2-用户有效性验证 var remoteResult = await _systemAdminRemoteService.selectAdminByAccount(phoneNumber); if (!remoteResult.IsSuccess) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, remoteResult.Msg); return; } else { // Claim 用于配置服务站点 [Authorize("anonymous")] var claimList = new List<Claim> { new Claim("grant_type", GrantTypeConstants.AdminSMSGrantType), new Claim("user_id", remoteResult.Response?.SnowflakeId ?? ""), new Claim("user_name", remoteResult.Response?.AccountName ?? ""), new Claim("name", remoteResult.Response?.AccountName ?? ""), new Claim("role", remoteResult.Response?.RoleId ?? ""), new Claim("phone_number", remoteResult.Response?.Account ?? "") }; context.Result = new GrantValidationResult( subject: phoneNumber, authenticationMethod: GrantTypeConstants.AdminSMSGrantType, claims: claimList); } } catch (Exception ex) { //context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant); context.Result = new GrantValidationResult() { IsError = true, Error = ex.Message }; } } } }
.Net下远程调用API Service如何实现?Java Oauth2认证实现,可私聊交流。