【.NET框架实战】IdentityServer4身份验证、授权
什么是身份认证
身份认证是指当客户端访问服务端资源时,验证客户端是否合法的一种机制
什么是授权
授权就是指当客户端经过身份认证后,能够有限的访问服务端资源的一种机制
为什么要使用身份验证和授权
为了保证服务端资源的安全,我们要理解必须从真实项目中去理解
身份认证和授权方式有哪些
1、Base认证
Base64编号认证 == https
2、Digest认证
MD5消息摘要认证 == https
3、Bearer认证
就是基于token(电子身份证)认证,讲用户信息等其他信息转换成为token,然后以token进行认证
token认证是一种无状态的认证方式,能够无限扩展,特别适合做单点登录
3.1 OAuth 2.0 ==== 授权方式的认证
3.2 JWT的也是一种身份认证
token 分两种类型
引用类型token == OAuth 2.0
无用户相关信息
自包含token
有用户相关信息 JWT === 地址,电话,id,等
在实际分布式项目中,大部分都是使用Bearer来进行身份认证和身份授权
在分布式项目或者微服务项目中,为了保证系统统一登录(SSO登录),
使用OpenID协议标准来规范身份认证功能 === 规范
同时使用OAuth 2.0协议标准来规范权限访问 === 规范
为了将身份认证(单点登录)和授权结合起来,所以出现了OpenID Connect协议标准 === 接口
OpenID Connect = OpenID + OAuth 2.0
SSO+OAuth 2.0可以实现单点登录和授权
IdentityServer4 也可以实现单点登录和授权
然后综合实现了这些框架就是今天要讲的IdentityServer4 身份认证服务器
如何在项目中使用IdentityServer4
IdentityServer4 是什么
IdentityServer是基于OpenID Connect协议标准的身份认证和授权程序,它实现了OpenID Connect 和 OAuth 2.0 协议。
IdentityServer4 功能
保护你的资源
使用本地帐户或通过外部身份提供程序对用户进行身份验证
提供会话管理和单点登录
管理和验证客户机
向客户发出标识和访问令牌
验证令牌
IdentityServer4 内部概念
下面对以下几个转用术语进行解释
用户(User)
用户是使用已注册的客户端(指在id4中已经注册)访问资源的人。
客户端(Client)
客户端就是从identityserver请求令牌的软件(你可以理解为一个app即可),既可以通过身份认证令牌来验证识别用户身份,又可以通过授权令牌来访问服务端的资源。但是客户端首先必须在申请令牌前已经在identityserver服务中注册过。
实际客户端不仅可以是Web应用程序,app或桌面应用程序(你就理解为pc端的软件即可),SPA,服务器进程等。
资源(Resources)
资源就是你想用identityserver保护的东东,可以是用户的身份数据或者api资源。
每一个资源都有一个唯一的名称,客户端使用这个唯一的名称来确定想访问哪一个资源(在访问之前,实际identityserver服务端已经配置好了哪个客户端可以访问哪个资源,所以你不必理解为客户端只要指定名称他们就可以随便访问任何一个资源)。
用户的身份信息实际由一组claim组成,例如姓名或者邮件都会包含在身份信息中(将来通过identityserver校验后都会返回给被调用的客户端)。
API资源就是客户端想要调用的功能(通常以json或xml的格式返回给客户端,例如webapi,wcf,webservice),通常通过webapi来建立模型,但是不一定是webapi,我刚才已经强调可以使其他类型的格式,这个要看具体的使用场景了。
身份令牌(顾名思义用于做身份认证,例如sso其实主要就是用于身份认证) id_token jwt
一个身份令牌指的就是对认证过程的描述。它至少要标识某个用户(Called the sub aka subject claim)的主身份信息,和该用户的认证时间和认证方式。但是身份令牌可以包含额外的身份数据,具体开发者可以自行设定,但是一般情况为了确保数据传输的效率,开发者一般不做过多额外的设置,大家也可以根据使用场景自行决定。
访问令牌(用于做客户端访问授权) access_token oauth 2.0
访问令牌允许客户端访问某个 API 资源。客户端请求到访问令牌,然后使用这个令牌来访问 API资源。访问令牌包含了客户端和用户(如果有的话,这取决于业务是否需要,但通常不必要)的相关信息,API通过这些令牌信息来授予客户端的数据访问权限。
IdentityServer4 角色逻辑关系图
微服务中使用IdentityServer4
IdentityServer4 官网地址
中文地址:http://www.identityserver.com.cn/Home/Detail/Zhengtizongshu
英文地址:https://identityserver4.readthedocs.io/en/3.1.0/
客户端认证模式
条件
1、IdentityServer4 授权服务器(IdentityServer)
2、客户端(Client)
3、微服务系统(Resources)
4、用户
步骤
1、NutGet下载IdentityServer4 在资源服务器中
1.1、在IOC容器中配置IdentityServer4
// 1、ioc容器中添加IdentityServer4
services.AddIdentityServer()
.AddDeveloperSigningCredential()// 1、用户登录配置
.AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
.AddInMemoryClients(Config.GetClients()) // 3、存储客户端(模式)
1.2、使用IdentityServer4
// 1、使用IdentityServe4
app.UseIdentityServer();
//2、添加静态资源访问
app.UseStaticFiles();
1.3、配置测试用户Config类
/// <summary>
/// 1、Identity测试使用
/// </summary>
public class Config
{
/// <summary>
/// 1、微服务API资源
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("TeamService", "TeamService api需要被保护")
};
}
/// <summary>
/// 2、客户端
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",
// 没有交互性用户,使用 clientid/secret 实现认证。
AllowedGrantTypes = GrantTypes.ClientCredentials,
// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 客户端有权访问的范围(Scopes)
AllowedScopes = { "TeamService" }
}
};
}
}
2、微服务配置
2.1 Nuget下载IdentityServer4.AccessTokenValidation
2.2 配置身份和授权认证(配置认证资源,让资源服务器进行保护)
// 6、校验AccessToken,从身份校验中心进行校验
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options => {
options.Authority = "http://localhost:5005"; // 1、授权中心地址
options.ApiName = "TeamService"; // 2、api名称(项目具体名称)
options.RequireHttpsMetadata = false; // 3、https元数据,不需要
});
2.3 开启身份认证
app.UseAuthentication(); // 1、使用身份验证
1
3、客户端配置
3.1 Nuget下载IdentityModel
3.2 调用代码即可
以上默认客户端认证模式
客户端密码认证模式
条件
1、用户Test
步骤
1、在Config中添加新客户端
new Client
{
ClientId = "client-password",
// 使用用户名密码交互式验证
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 客户端有权访问的范围(Scopes)
AllowedScopes = { "TeamService" }
},
2、在config中添加test用户
/// <summary>
/// 客户端下面的用户
/// </summary>
/// <returns></returns>
public static List<TestUser> GetUsers()
{
return new List<TestUser>()
{
new TestUser
{
SubjectId="1",
Username="tony",
Password="123456"
}
};
}
3、在startup中添加配置
// 1、ioc容器中添加IdentityServer4
services.AddIdentityServer()
.AddDeveloperSigningCredential()// 1、用户登录配置
.AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
.AddInMemoryClients(Config.GetClients()) // 3、存储客户端(模式)
.AddTestUsers(Config.GetUsers())// 4、添加登录用户(模式)
4、 开始调用
在代码中已经写好调用过程
授权码模式认证(通过code获取assess_token)
条件
1、OpenIdConnect
步骤
1、Nuget下载Microsoft.AspNetCore.Authentication.OpenIdConnect
2、认证服务器配置
2.1 在config中添加openid身份资源声明
public static IEnumerable<IdentityResource> Ids => new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
2.2 在config中添加客户端
// openid客户端
new Client
{
ClientId="client-code",
ClientSecrets={new Secret("secret".Sha256())},
AllowedGrantTypes=GrantTypes.Code,
RequireConsent=false,
RequirePkce=true,
RedirectUris={ "https://localhost:5006/signin-oidc"},
PostLogoutRedirectUris={ "https://localhost:5006/signout-callback-oidc"},
AllowedScopes=new List<string>{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"TeamService" // 启用对刷新令牌的支持
},
// 增加授权访问
AllowOfflineAccess=true
}
2.3 添加openidtest用户
// openid 身份验证
new TestUser{SubjectId = "818727", Username = "张三", Password = "123456",
Claims =
{
new Claim(JwtClaimTypes.Name, "张三"),
new Claim(JwtClaimTypes.GivenName, "三"),
new Claim(JwtClaimTypes.FamilyName, "张"),
new Claim(JwtClaimTypes.Email, "zhangsan@email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, "http://zhangsan.com"),
// new Claim(JwtClaimTypes.Address, @"{ '城市': '杭州', '邮政编码': '310000' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
}
}
2.4、在startup中添加配置
// 1、ioc容器中添加IdentityServer4
services.AddIdentityServer()
.AddDeveloperSigningCredential()// 1、用户登录配置
.AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
.AddInMemoryClients(Config.GetClients()) // 3、存储客户端(模式)
.AddTestUsers(Config.GetUsers())// 4、添加登录用户(模式)
.AddInMemoryIdentityResources(Config.Ids); // 4、使用openid模式
2.5 准备登陆ui页面
代码中已经准备好
3、客户端配置
3.1 startup.cs文件中配置
// 1、添加身份验证
// 我们使用cookie来本地登录用户(通过“Cookies”作为DefaultScheme),并且将DefaultChallengeScheme设置为oidc。因为当我们需要用户登录时,我们将使用OpenID Connect协议。
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
// 添加可以处理cookie的处理程序
.AddCookie("Cookies")
// 用于配置执行OpenID Connect协议的处理程序
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5005"; // 受信任令牌服务地址
options.RequireHttpsMetadata = false;
options.ClientId = "client-code";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true; // 用于将来自IdentityServer的令牌保留在cookie中
// 1、添加授权访问api的支持
options.Scope.Add("TeamService");
options.Scope.Add("offline_access");
});
3.2 使用配置
// 1、使用静态页面
app.UseStaticFiles();
// 2、开启身份验证
app.UseAuthentication();
IdentityServer4持久化
主要是将资源数据,用户数据,身份数据全部进行放在数据库进行存储。
条件
1、ConfigurationDbContext
用于配置数据,如clients,resources和scopes。
2、PersistedGrantDbContext
用于临时操作数据,如授权代码和刷新令牌
步骤
1、Nuge安装IdentityServer4.EntityFramework
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
2、重新配置Startup.cs文件
// 2、资源客户端持久化操作 var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddIdentityServer() .AddConfigurationStore(options => { options.ConfigureDbContext = builder => { builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)); }; }) .AddOperationalStore(options => { options.ConfigureDbContext = builder => { builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)); }; }) .AddTestUsers(Config.GetUsers()) .AddDeveloperSigningCredential();
3、appsettings.json中的配置
{ "ConnectionStrings": { "DefaultConnection": "Data Source=.;Initial Catalog=IndentityServer4;Persist Security Info=True;User ID=sa;Password=tony" } }
4、执行迁移脚本
dotnet ef migrations add PersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add ConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
5、迁移脚本生成数据库和表
dotnet ef database update -c PersistedGrantDbContext
dotnet ef database update -c ConfigurationDbContext
6、示例数据存储
// 1、将config中数据存储起来
private void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope()) { serviceScope.ServiceProvider.GetService<PersistedGrantDbContext>().Database.Migrate(); var context = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>(); context.Database.Migrate(); if (!context.Clients.Any()) { foreach (var client in Config.GetClients()) { context.Clients.Add(client.ToEntity()); } context.SaveChanges(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.Ids) { context.IdentityResources.Add(resource.ToEntity()); } context.SaveChanges(); } if (!context.ApiResources.Any()) { foreach (var resource in Config.GetApiResources()) { context.ApiResources.Add(resource.ToEntity()); } context.SaveChanges(); } } }
6、客户端进行访问
IdentityServer4 用户进行持久化(数据存储)
条件
1、Identity
步骤
1、Nuget安装Microsoft.AspNetCore.Identity.EntityFrameworkCore
2、添加上下文类
public class IdentityServerUserDbContext : IdentityDbContext<IdentityUser> { public IdentityServerUserDbContext(DbContextOptions<IdentityServerUserDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); } }
3、startup.cs文件配置
services.AddDbContext<IdentityServerUserDbContext>(options => { options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); });
// 1.1 添加用户
services.AddIdentity<IdentityUser, IdentityRole>(options => {
// 1.2 密码复杂度配置
options.Password.RequireDigit = true; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; }) .AddEntityFrameworkStores<IdentityServerUserDbContext>() .AddDefaultTokenProviders();
4、执行迁移脚本
dotnet ef migrations add IdentityServerUserDbMigration -c IdentityServerUserDbContext -o Data/Migrations/IdentityServer/IdentityServerUserDb
1
5、迁移脚本生成数据库和表
dotnet ef database update -c IdentityServerUserDbContext
1
6、示例数据存储
// 2、将用户中数据存储起来
private void InitializeUserDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var context = serviceScope.ServiceProvider.GetService<IdentityServerUserDbContext>(); context.Database.Migrate(); var userManager = serviceScope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>(); var idnetityUser = userManager.FindByNameAsync("tony").Result; if (idnetityUser == null) { idnetityUser = new IdentityUser { UserName = "zhangsan", Email = "zhangsan@email.com" }; var result = userManager.CreateAsync(idnetityUser, "123456").Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } result = userManager.AddClaimsAsync(idnetityUser, new Claim[] { new Claim(JwtClaimTypes.Name, "tony"), new Claim(JwtClaimTypes.GivenName, "tony"), new Claim(JwtClaimTypes.FamilyName, "tony"), new Claim(JwtClaimTypes.Email, "tony@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://tony.com") }).Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } } } }
6、AccountController修改Login代码
public class AccountController : Controller { private UserManager<ApplicationUser> _userManager; private SignInManager<ApplicationUser> _signInManager; private IIdentityServerInteractionService _interaction; public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IIdentityServerInteractionService interaction) { _userManager = userManager; _signInManager = signInManager; _interaction = interaction; } public IActionResult Login(string returnUrl) { ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] public async Task<IActionResult> Login(LoginViewModel loginViewModel, string returnUrl) { // check if we are in the context of an authorization request var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); // the user clicked the "cancel" button if (button != "login") { if (context != null) { // if the user cancels, send a result back into IdentityServer as if they // denied the consent (even if this client does not require consent). // this will send back an access denied OIDC error response to the client. await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null if (await _clientStore.IsPkceClientAsync(context.ClientId)) { // if the client is PKCE then we assume it's native, so this change in how to // return the response is for better UX for the end user. return this.LoadingPage("Redirect", model.ReturnUrl); } return Redirect(model.ReturnUrl); } else { // since we don't have a valid context, then we just go back to the home page return Redirect("~/"); } } if (ModelState.IsValid) { var user = await _userManager.FindByNameAsync(model.Username); if (user == null) { ModelState.AddModelError(nameof(model.Username), $"用户名 {model.Username} not exists"); } else { if (await _userManager.CheckPasswordAsync(user, model.Password)) { AuthenticationProperties props = null; if (model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(30)) }; } var isuser = new IdentityServerUser(user.Id) { DisplayName = user.UserName }; await HttpContext.SignInAsync(isuser, props); if (_interaction.IsValidReturnUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); } ModelState.AddModelError(nameof(model.Password), "密码错误"); } } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); } }
ocelot集成 IdentityServer4
条件
1、IdentityServer4
步骤
1、Nuget安装IdentityServer4.AccessTokenValidation
2、在startup中文件配置IndentityServer4
// 1、配置IdentityServer
var authenticationProviderKey = "OcelotKey"; services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(options => { options.Authority = "http://localhost:5005"; // 1、授权中心地址 options.ApiName = "TeamService"; // 2、api名称(项目具体名称) options.RequireHttpsMetadata = false; // 3、https元数据,不需要 options.SupportedTokens = SupportedTokens.Both; });
3、 在ocelot.json中配置身份验证
"ReRoutes": [ { "AuthenticationOptions": { "AuthenticationProviderKey": "OcelotKey", "AllowedScopes": [] } } ]
ocelot集成 IdentityServer4 配置封装
条件
1、IdentityServerOptions类
步骤
1、创建IdentityServerOptions类
/// <summary> /// IdentityServer的配置选项 /// </summary> public class IdentityServerOptions { /// <summary> /// 授权服务器的地址 /// </summary> public int AuthorityAddress { get; set; } /// <summary> /// access_token的类型,获取access_token的时候返回参数中的token_type一致 /// </summary> public string IdentityScheme { get; set; } /// <summary> /// 资源名称,认证服务注册的资源列表名称一致, /// </summary> public string ResourceName { get; set; } }
2、配置IdentityServerOptions
// 1、配置IdentityServer 增加配置选项
var identityServerOptions = new IdentityServerOptions(); Configuration.Bind("IdentityServerOptions", identityServerOptions); var authenticationProviderKey = "OcelotKey"; services.AddAuthentication(identityServerOptions.IdentityScheme) .AddIdentityServerAuthentication(options => { options.Authority = identityServerOptions.AuthorityAddress; // 1、授权中心地址 options.ApiName = identityServerOptions.ResourceName; // 2、api名称(项目具体名称) options.RequireHttpsMetadata = false; // 3、https元数据,不需要 options.SupportedTokens = SupportedTokens.Both; });
ocelot动态路由
条件
1、consul
步骤
1、动态路由配置
{ "ReRoutes": [], "Aggregates": [], "GlobalConfiguration": { "RequestIdKey": null, "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, "Type": "Consul", "Token": null, "ConfigurationKey": null }, "RateLimitOptions": { "ClientIdHeader": "ClientId", "QuotaExceededMessage": null, "RateLimitCounterPrefix": "ocelot", "DisableRateLimitHeaders": false, "HttpStatusCode": 429 }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 0, "DurationOfBreak": 0, "TimeoutValue": 0 }, "BaseUrl": null, "LoadBalancerOptions": { "Type": "LeastConnection", "Key": null, "Expiry": 0 }, "DownstreamScheme": "https", "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false } } }
2、动态路由服务限流配置
{ "DynamicReRoutes": [ { "ServiceName": "TeamService", "RateLimitRule": { "ClientWhitelist": [], "EnableRateLimiting": true, "Period": "1s", "PeriodTimespan": 3000, "Limit": 3 } } ], "GlobalConfiguration": { "RequestIdKey": null, "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, "Type": "Consul" }, "RateLimitOptions": { "ClientIdHeader": "ClientId", "QuotaExceededMessage": "", "RateLimitCounterPrefix": "", "DisableRateLimitHeaders": false, "HttpStatusCode": 428 }, "DownstreamScheme": "http" } }
https://www.jianshu.com/p/56b577d8f786