10. 【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--微服务基础工具与技术--Ocelot 网关--认证
在微服务架构中,通过在网关层实现身份认证、权限校验和数据加密,可以有效防范恶意攻击和非法访问,保障内部服务安全。采用JWT、OAuth等主流认证机制,使每次请求均经过严格验证,降低安全漏洞风险。同时,统一的安全管理还便于监控和日志审计,及时发现异常行为并做出响应。完善安全策略不仅增强系统稳定性,还能构建可靠访问环境,切实保护企业数据安全。
一、JWT认证详解
1.1 JWT简介与工作原理
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在网络环境中以紧凑、自包含的方式安全传递信息。它基于JSON格式,并通过数字签名保证数据完整性与真实性,广泛应用于身份验证和信息交换。JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),并以“.”分隔。头部包含令牌类型及签名算法,如HS256或RSA,经过Base64Url编码后形成第一部分。载荷存储标准声明(如发行者 iss
、过期时间 exp
、主题 sub
)和自定义数据,用于传递身份信息,但未加密,不应存敏感数据。签名部分使用密钥对前两部分加密,确保数据未被篡改。
JWT的主要优势是无状态,无需服务器存储会话信息,适用于分布式系统和负载均衡。其轻量特性使其易于在HTTP请求头中传输,同时基于JSON格式,具备跨平台兼容性,方便不同系统统一认证。JWT常与OAuth2、OpenID Connect结合,用于安全授权,并内置过期机制(exp)防止令牌滥用。在微服务架构中,各服务节点只需验证JWT即可判定请求合法性,提升系统扩展性。
使用JWT时需确保密钥安全,防止令牌泄露,并选择合适的签名算法(如HS256或RS256)。可结合令牌黑名单、短生命周期策略等增强安全性,防止令牌被截获后重放。
1.2 JWT在Ocelot中的应用
在Ocelot网关中,JWT实现了高效的用户身份认证与授权。客户端在向网关发起请求时,会在HTTP请求头中携带JWT(格式为“Bearer {token}”)。Ocelot作为统一入口,会拦截请求并通过认证中间件解析、验证令牌,包括检查令牌的存在性、格式、签名、过期时间等。验证成功后,Ocelot将请求转发至后端微服务,否则返回未授权响应。这种机制确保只有合法认证的请求能进入系统,同时便于集中管理和安全监控。
JWT的生成与验证通常由认证服务器(如IdentityServer)负责。当用户成功认证后,服务器使用指定算法(如HS256或RS256)生成JWT,其中包含头部(类型与算法)、载荷(用户标识、权限、过期时间等)和签名(用于数据完整性保护)。生成的JWT返回给客户端,并在后续请求中携带。Ocelot接收请求后,通过密钥或公钥验证JWT,解码并校验签名及过期时间,确保请求合法。一旦通过,网关允许请求进入微服务网络,从而实现安全、高效的身份认证管理。
二、使用IdentityServer进行认证
2.1 IdentityServer简介
IdentityServer是一个基于.NET的开源身份认证与授权框架,主要用于单点登录、API保护和安全令牌管理。它帮助开发者在微服务架构中构建统一的认证中心,简化身份验证和授权流程。
作为安全令牌服务(STS),IdentityServer生成JWT等令牌,在微服务间传递用户信息和权限。它支持单点登录(SSO),避免重复认证,并允许开发者灵活配置客户端、API资源和授权范围。其支持授权码、隐式、混合、客户端凭据、资源所有者密码等多种认证流程,并内置令牌管理、刷新、撤销等安全机制,确保认证体系安全稳定。
IdentityServer基于OAuth2和OpenID Connect协议,OAuth2负责授权,允许第三方应用安全访问资源,而OIDC扩展OAuth2,引入ID Token,实现用户身份验证。IdentityServer提供访问令牌管理,并支持用户信息查询接口,确保跨系统认证的便捷性和安全性。通过标准化流程,IdentityServer提升了客户端兼容性,简化系统集成,使企业能够构建高效、可信的身份认证体系。
2.2 IdentityServer与JWT认证的关系
IdentityServer与JWT认证关系密切,它通过JWT(JSON Web Token)作为身份验证和授权的核心机制,为分布式系统提供安全、无状态的令牌管理。IdentityServer作为OAuth2和OpenID Connect的实现,负责用户身份验证,并在授权后颁发JWT令牌,供客户端访问受保护资源时使用。
在身份认证过程中,用户通过IdentityServer验证身份后,服务器会生成包含用户信息、权限范围等数据的JWT,并返回给客户端。客户端在后续请求中,将该JWT附加到HTTP请求头中,API网关或后端服务通过验证JWT签名和有效期,确保请求合法性和安全性。
JWT的无状态特性使得服务端无需存储会话信息,提升了系统的扩展性。IdentityServer通过OAuth2支持JWT的生成、刷新和撤销,使得身份认证和授权更加标准化、安全和高效。综合而言,IdentityServer利用JWT实现安全的身份认证和授权管理,确保微服务架构下的高效通信与访问控制。
2.3 集成步骤
在Ocelot网关中集成IdentityServer主要涉及身份认证与授权配置,确保所有经过网关的请求都需要进行身份验证。
-
搭建IdentityServer
在独立的认证服务器上搭建 IdentityServer4,配置客户端、API资源、授权范围(Scopes),确保IdentityServer能够正确颁发 JWT令牌,并支持OAuth2或OpenID Connect协议。 -
配置Ocelot网关
在ocelot.json
配置文件中,配置要网关路由,相关内容已经在ocelot 路由文章中已经讲解了,这里就不在重复讲解。 -
在Ocelot中启用认证
首先在Program.cs
中,添加JWT身份验证:builder.Services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "http://localhost:5000"; // IdentityServer地址 options.RequireHttpsMetadata = false; options.Audience = "api1"; // 资源API });
然后注册Ocelot网关:
builder.Services.AddOcelot();
三、案例演示
我们根据上一小节的Ocelot网关中集成IdentityServer的步骤来实现一个简单的例子。
新建解决方案OcelotIdentityServerDemo,并在这个解决方案下新建三个Web Api类库:Weather、Authentication和Gateway。Weather是业务服务,用来获取天气,Authentication作为认证服务器使用,Gateway作为网关使用。
3.1 Authentication 认证服务
-
引入IdentityServer包
打开命令行切换到 Authentication Web Api 类库所在文件夹,执行如下命令:
dotnet add package OpenIddict.AspNetCore dotnet add package OpenIddict.EntityFrameworkCore dotnet add package OpenIddict.Validation.AspNetCore
在上面的命令中,我们在类库中添加了
OpenIddict.AspNetCore
、OpenIddict.EntityFrameworkCore
和OpenIddict.Validation.AspNetCore
。这三个包的作用如下:- OpenIddict.AspNetCore
这个包将 OpenIddict 与 ASP.NET Core 无缝集成,提供构建认证服务器所需的各类终结点(如 token 颁发端点)和中间件支持。它简化了配置授权流程、处理请求、签发和撤销令牌等工作,使开发者能快速搭建符合 OAuth 2.0 和 OpenID Connect 标准的认证服务。 - OpenIddict.EntityFrameworkCore
该包利用 Entity Framework Core 作为 OpenIddict 的数据存储解决方案。它提供了必要的实体映射和数据库上下文配置,用于持久化应用信息、授权记录、令牌数据等安全相关的数据。通过这一集成,开发者可以方便地管理和查询认证数据,同时享受 EF Core 提供的数据迁移和操作功能。 - OpenIddict.Validation.AspNetCore
这个包主要用于在 ASP.NET Core 环境中对由 OpenIddict 颁发的令牌进行验证。它将 OpenIddict 的令牌验证逻辑集成到 ASP.NET Core 的身份认证中间件中,确保每个访问受保护资源的请求都能自动校验令牌的有效性和完整性,从而保障 API 的安全访问。
- OpenIddict.AspNetCore
-
配置Authentication服务
在
Program.cs
中添加 OpenIddict 服务,代码如下:builder.Services.AddOpenIddict() .AddCore(options => { // 使用 EntityFrameworkCore作为数据源 options.UseEntityFrameworkCore() .UseDbContext<IdentityServerDbContext>(); }) .AddServer(options => { // 设置令牌端点 options.SetTokenEndpointUris("/connect/token"); // 启用密码模式 options.AllowPasswordFlow() // 开启密码模式 .AllowClientCredentialsFlow() .AllowRefreshTokenFlow();// 开启刷新令牌 options.RegisterScopes("api"); // 使用开发环境下的临时密钥(生产环境请使用持久化证书) options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); options.AddSigningKey( new SymmetricSecurityKey( Convert.FromBase64String("SguGSpvaRLMwnmnxiBHRdSxRpBDSiD8+8J1qp1czuD8="))); options.AddEncryptionKey(new SymmetricSecurityKey(Convert.FromBase64String("na8LnVekSu5b3fgdUhyo+KuLTMVGYLtgHrTTpKCB5VY="))); // 集成 ASP.NET Core options.UseAspNetCore() .EnableAuthorizationEndpointPassthrough() .EnableTokenEndpointPassthrough() .DisableTransportSecurityRequirement();// 开发模式下金庸HTTPS }) .AddValidation(options => { // 使用本地服务器进行验证 options.UseLocalServer(); // 使用 AspNetCore 进行验证 options.UseAspNetCore(); }); builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
这段代码利用 OpenIddict 在 ASP.NET Core 应用中构建一个完整的 OAuth 2.0 授权和资源管理体系。它分为几个关键部分,每个部分都负责不同的功能,共同协作以实现安全可靠的身份验证和授权。
首先,
builder.Services.AddOpenIddict()
是整个配置的起点。它像一个基石,将 OpenIddict 的核心服务注册到 ASP.NET Core 的依赖注入容器中。这使得应用能够使用 OpenIddict 提供的各种功能,比如定义客户端、管理授权、颁发和验证令牌等。可以把它想象成激活了 OpenIddict 的总开关,后续的配置都是在此基础上进行的。接下来,
.AddCore(options => { ... })
用于配置 OpenIddict 的核心数据存储。这部分代码指定了使用 Entity Framework Core 作为 OpenIddict 的数据持久化方案。options.UseEntityFrameworkCore()
这行代码告诉 OpenIddict,所有关于客户端、授权、Scope 和令牌的信息都将存储在数据库中。这个设置对于生产环境至关重要,因为它可以确保数据在应用重启后仍然存在。options.UseDbContext<IdentityServerDbContext>()
则进一步指定了用于 EF Core 的 DbContext。IdentityServerDbContext
必须是一个继承自DbContext
的类,并且包含 OpenIddict 所需的实体,比如OpenIddictEntityFrameworkCoreApplication
(客户端信息)、OpenIddictEntityFrameworkCoreAuthorization
(授权信息)、OpenIddictEntityFrameworkCoreScope
(Scope 信息) 和OpenIddictEntityFrameworkCoreToken
(令牌信息)。我们需要根据数据库结构来定义这个 DbContext,并确保它与 OpenIddict 的期望相符。.AddServer(options => { ... })
是配置 OAuth 2.0 授权服务器的关键部分。它定义了授权服务器的行为和端点。options.SetTokenEndpointUris("/connect/token")
设置了令牌端点的 URI,客户端会向这个端点发送请求以获取访问令牌。可以把这个端点看作是授权服务器的核心入口,所有令牌相关的请求都会经过这里。options.AllowPasswordFlow()
、.AllowClientCredentialsFlow()
和.AllowRefreshTokenFlow()
分别启用了 OAuth 2.0 规范中定义的几种授权模式:密码模式、客户端凭据模式和刷新令牌模式。密码模式允许客户端直接使用用户的用户名和密码来获取令牌,客户端凭据模式允许客户端使用自己的身份信息(客户端 ID 和密钥)来获取令牌,而刷新令牌模式则允许客户端使用刷新令牌来获取新的访问令牌,避免用户重复登录。Tip:需要注意的是,密码模式在安全性方面有一些争议,通常不建议在生产环境中使用,除非有充分的理由并且采取了额外的安全措施。
options.RegisterScopes("api")
注册了一个名为 “api” 的 Scope,Scope 用于限制访问令牌的权限。客户端在请求令牌时可以指定所需的 Scope,授权服务器会根据这些 Scope 来生成具有相应权限的令牌。options.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate()
在开发环境中配置了用于加密和签名令牌的证书。这些方法添加了临时的、自动生成的证书,方便开发和测试。然而,在生产环境中,绝对不能使用这种方式,必须使用由受信任的证书颁发机构颁发的证书,并妥善保管私钥。options.AddSigningKey(...)
和options.AddEncryptionKey(...)
使用对称密钥来签名和加密令牌。Tip:这里直接在代码中硬编码了密钥,这是一种非常不安全的做法,绝对不能在生产环境中使用。在生产环境中,应该使用更安全的密钥管理方案,比如使用 Azure Key Vault 或 HashiCorp Vault 等专门的密钥管理服务来存储和管理密钥。
options.UseAspNetCore()
将 OpenIddict 与 ASP.NET Core 集成,使得 OpenIddict 可以利用 ASP.NET Core 的各种功能,比如路由、中间件和依赖注入。.EnableAuthorizationEndpointPassthrough()
和.EnableTokenEndpointPassthrough()
启用了授权和令牌端点的直通模式,我们可以使用 ASP.NET Core 的路由机制来处理这些端点,而不是完全依赖 OpenIddict 的默认处理方式。.DisableTransportSecurityRequirement()
禁用了 HTTPS 传输安全要求。这只应该在开发环境中进行,因为在生产环境中,必须使用 HTTPS 来保护敏感数据的传输。.AddValidation(options => { ... })
配置了 OpenIddict 的令牌验证功能。这部分代码定义了 API 资源服务器如何验证客户端提供的访问令牌。options.UseLocalServer()
配置使用本地授权服务器进行令牌验证,API 资源服务器会与同一应用中的授权服务器通信来验证令牌的有效性。通常用于单体应用或微服务架构中的内部服务之间的验证。最后,
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)
将 OpenIddict 的验证方案添加到 ASP.NET Core 的身份验证管道中。可以使用 ASP.NET Core 的身份验证机制来保护 API 资源,只有持有有效 OpenIddict 令牌的客户端才能访问这些资源。 -
配置数据库上下文
在上一小节中, 我们指定了数据库上下文
IdentityServerDbContext
,在这一小节中我们就来创建这个数据库上下文。
首先,安装数据库提供程序Pomelo.EntityFrameworkCore.MySql
,在命令行输入如下命令:dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Pomelo.EntityFrameworkCore.MySql
然后,创建自定义 DbContext
IdentityServerDbContext
,重写OnModelCreating
方法,其中调用builder.UseOpenIddict()
注册 OpenIddict 的实体映射,重写OnConfiguring
方法配置数据库连接。代码如下:using Microsoft.EntityFrameworkCore; namespace Authentication; /// <summary> /// IdentityServer 数据库上下文 /// </summary> public class IdentityServerDbContext : DbContext { /// <summary> /// 数据库连接配置 /// </summary> IConfiguration _dbConfig; /// <summary> /// 构造函数 /// </summary> /// <param name="dbConfig"></param> public IdentityServerDbContext(IConfiguration dbConfig) { _dbConfig = dbConfig; } /// <summary> /// 配置数据库模型 /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 配置 OpenIddict modelBuilder.UseOpenIddict(); } /// <summary> /// 数据库连接配置 /// </summary> /// <param name="optionsBuilder"></param> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var serverVersion = ServerVersion.AutoDetect(_dbConfig.GetConnectionString("MySQLConnection")); optionsBuilder.UseMySql(_dbConfig.GetConnectionString("MySQLConnection"), serverVersion); } }
-
配置数据库链接字符串
打开appsettings.json配置文件,添加数据库连接字符串,代码如下:
"ConnectionStrings": { "MySQLConnection": "server=14.103.224.141;port=3308;database=IdentityServerDemo;user=root;pwd=123*asdasd;" }
-
数据库迁移
所有配置完成后,使用 Entity Framework Core 的迁移命令生成数据库结构,这将创建 OpenIddict 所需的表结构。
dotnet ef migrations add InitialMigration dotnet ef database update
-
设置种子数据
在Authentication项目根目录下新建
SeedData
类,这个类继承子IHostedService
,在里面编写种子数据的代码。public class SeedData: IHostedService { private readonly IServiceProvider _serviceProvider; public SeedData(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task StartAsync(CancellationToken cancellationToken) { using var scope = _serviceProvider.CreateScope(); var applicationManager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>(); // 检查是否存在指定的 client_id if (await applicationManager.FindByClientIdAsync("client_id1", cancellationToken) is null) { var descriptor = new OpenIddictApplicationDescriptor { ClientId = "client_id1", ClientSecret = "client_secret", // 根据需求设置密钥 DisplayName = "示例客户端应用", Permissions = { OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.GrantTypes.Password, // 允许密码模式 OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, OpenIddictConstants.Permissions.Prefixes.Scope + "api" // 允许访问的 API 作用域 } }; await applicationManager.CreateAsync(descriptor, cancellationToken); } } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
这段代码在 ASP.NET Core 中使用 OpenIddict 配置 OpenID Connect 客户端,
SeedData
实现了IHostedService
,在应用程序启动时创建客户端配置。通过构造函数注入IServiceProvider
,在StartAsync
中创建服务范围,解析IOpenIddictApplicationManager
,并检查是否存在指定的ClientId
。如果不存在,创建
OpenIddictApplicationDescriptor
定义客户端配置:包括ClientId
(客户端 ID)、ClientSecret
(密钥)、DisplayName
(显示名称)和权限。权限包括访问 Token 端点(Endpoints.Token
)、密码模式授权(GrantTypes.Password
)、客户端凭据授权(GrantTypes.ClientCredentials
)和访问名为api
的作用域。通过applicationManager.CreateAsync()
将配置存储在 OpenIddict 的配置存储中。StopAsync
为空,不需要在程序关闭时执行清理。接着,将
SeedData
类引入到Program
类中:builder.Services.AddHostedService<SeedData>();
-
实现令牌端点
在
Program
类中设置了获取令牌的端点,在这一小节,就来实现这个端点。首先创建AuthorizationController
控制器,接着在其中编写令牌端点的代码。using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using System.Security.Claims; using Microsoft.AspNetCore; namespace Authentication.Controllers { /// <summary> /// 授权控制器 /// </summary> [Route("/connect")] [ApiController] public class AuthorizationController : ControllerBase { /// <summary> /// 令牌端点 /// </summary> /// <returns></returns> /// <exception cref="InvalidOperationException"></exception> [HttpPost("token")] public IActionResult Exchange() { var request = HttpContext.GetOpenIddictServerRequest(); // 处理资源所有者密码模式 if (request.IsPasswordGrantType()) { // 示例:仅支持用户名 "alice" 和密码 "password123" if (request.Username != "alice" || request.Password != "password123") { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户名或密码错误。" })); } // 创建用户身份并添加必要的声明 var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); identity.AddClaim(OpenIddictConstants.Claims.Subject, request.Username); identity.AddClaim(OpenIddictConstants.Claims.Name, "Alice"); identity.AddClaim(OpenIddictConstants.Claims.Audience, "api"); // 添加 aud 声明 // 创建 ClaimsPrincipal,并设置请求的范围 var principal = new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); // 返回 SignIn 结果,OpenIddict 将生成访问令牌 return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // 处理客户端凭证模式 else if (request.IsClientCredentialsGrantType()) { // 客户端凭证模式下,客户端身份已由 OpenIddict 验证器验证,直接使用 client_id 作为主题标识 var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException()); identity.AddClaim(OpenIddictConstants.Claims.Audience, "api"); // 添加 aud 声明 var principal = new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // 不支持的授权类型 else { return BadRequest(new { error = OpenIddictConstants.Errors.UnsupportedGrantType, error_description = "不支持的授权类型。" }); } } } }
Exchange
方法用来处理客户端通过 OpenID Connect 发起的令牌请求。首先通过HttpContext.GetOpenIddictServerRequest()
获取当前的 OpenIddict 请求对象request
。这个对象封装了 OpenID Connect 协议中的授权请求参数,例如授权类型(grant_type)、客户端 ID(client_id)、作用域(scope)、用户名(username)和密码(password)等。接下来,代码根据授权类型(grant_type)来决定如何处理授权请求。如果授权类型是“资源所有者密码模式”(Password Grant),通过request.IsPasswordGrantType()
进行判断。在密码模式下,客户端需要提供用户名和密码来直接获取访问令牌。 在这里,代码接受用户名为 “alice” 且密码为 “password123” 的请求。
如果用户名或密码不正确,返回
Forbid()
结果。Forbid()
方法表示认证失败,使用OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
作为认证方案,同时设置 OpenIddict 定义的错误代码和错误描述。AuthenticationProperties
被用来传递错误信息,其中OpenIddictServerAspNetCoreConstants.Properties.Error
被设置为"invalid_grant"
,表示授权失败,OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription
设置为 “用户名或密码错误。”。如果用户名和密码正确,创建一个
ClaimsIdentity
对象,使用OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
作为身份验证方案。然后通过identity.AddClaim()
方法为身份对象添加声明(Claim):
-OpenIddictConstants.Claims.Subject
:设置sub
(主题)声明,表示身份的唯一标识,这里设置为用户名alice
。
-OpenIddictConstants.Claims.Name
:设置name
声明,表示用户的显示名称,设置为"Alice"
。
-OpenIddictConstants.Claims.Audience
:设置aud
(受众)声明,表示访问的资源,设置为"api"
。然后,创建
ClaimsPrincipal
对象,将前面创建的ClaimsIdentity
作为参数传入。通过principal.SetScopes(request.GetScopes())
方法设置请求的作用域(scope),OpenIddict 将根据这些作用域生成访问令牌中包含的权限范围。最后,调用
SignIn()
方法,传入principal
和OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
作为参数。SignIn()
方法表示认证成功,OpenIddict 将根据传入的声明信息生成访问令牌并返回给客户端。如果授权类型是“客户端凭证模式”(Client Credentials Grant),则通过
request.IsClientCredentialsGrantType()
进行判断。在客户端凭证模式下,客户端不涉及用户身份,直接通过client_id
和client_secret
进行认证。由于 OpenIddict 的验证器已经在此之前完成了对client_id
和client_secret
的验证,因此代码直接创建一个新的ClaimsIdentity
对象,并通过identity.AddClaim()
方法添加声明:
-OpenIddictConstants.Claims.Subject
:设置sub
(主题)声明,表示客户端身份,设置为request.ClientId
。如果request.ClientId
为空,抛出InvalidOperationException
。
-OpenIddictConstants.Claims.Audience
:设置aud
(受众)声明,表示访问的资源,设置为"api"
。最后,创建
ClaimsPrincipal
,并设置请求的作用域。调用SignIn()
方法,OpenIddict 将生成访问令牌返回给客户端。这段代码,密码模式主要用于涉及用户身份的场景,如通过用户名和密码登录,获取用户授权的访问令牌。而客户端凭证模式用于服务间通信,不涉及用户身份,仅通过客户端凭据授权。
3.2 Gateway 服务
-
网关配置
命令行切换到 Gateway Web Api 类库所在文件夹,将网关Ocelot引入进来:
dotnet add package Ocelot
在项目中
Program.cs
中,配置 JWT Bearer 认证,将 Authority 指向 OpenIddict 身份认证服务器地址,并注册 Ocelot:// 配置 JWT 认证(验证来自认证服务器的令牌) builder.Services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "https://localhost:5002"; // 认证服务器地址 options.RequireHttpsMetadata = false; // 开发环境下允许 HTTP options.Audience = "api"; // 与业务服务器要求的受众一致 options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, ValidateIssuerSigningKey = false, // 忽略 kid 检查 ValidAudience = "api", ValidIssuer = "http://localhost:5002", IssuerSigningKey = new SymmetricSecurityKey( Convert.FromBase64String("SguGSpvaRLMwnmnxiBHRdSxRpBDSiD8+8J1qp1czuD8=")), TokenDecryptionKey = new SymmetricSecurityKey( Convert.FromBase64String("na8LnVekSu5b3fgdUhyo+KuLTMVGYLtgHrTTpKCB5VY=")), ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512 } }; }); // 注册 Ocelot 服务 builder.Services.AddOcelot();
这段代码配置了JWT认证,并注册了Ocelot服务。首先,
builder.Services.AddAuthentication("Bearer").AddJwtBearer("Bearer", options => { ... });
这部分用于添加Bearer类型的JWT认证。options.Authority = "https://localhost:5002";
设置了认证服务器的地址,表明token是从这个地址签发的。options.RequireHttpsMetadata = false;
在开发环境下允许使用HTTP,正式环境应该设置为true以保证安全性。options.Audience = "api";
指定了业务服务器期望的受众,确保只有目标为 “api” 的token才被接受。options.TokenValidationParameters
定义了token验证的各种参数,其中ValidateIssuer = false
,ValidateAudience = false
,ValidateLifetime = false
,ValidateIssuerSigningKey = false
这些参数都被设置为false,这意味着发行者、受众、生命周期和签名密钥的验证都被禁用了,这通常不建议在生产环境中使用。ValidAudience = "api"
,ValidIssuer = "http://localhost:5002"
指定了合法的受众和发行者,但由于前面的验证参数被禁用,这些设置实际上没有生效。IssuerSigningKey
和TokenDecryptionKey
使用了对称密钥,这些密钥从Base64字符串转换而来,用于签名验证和token解密。ValidAlgorithms
指定了允许的加密算法,包括HmacSha256
,Aes256KW
和Aes256CbcHmacSha512
。最后,builder.Services.AddOcelot();
注册了Ocelot服务,Ocelot是一个API网关,用于路由和管理API请求。总的来说,这段代码配置了一个JWT认证方案,但由于禁用了许多关键验证步骤,可能存在安全风险,并注册了Ocelot API网关服务。 -
配置路由
接着,进行Ocelot 网关配置,在项目的
ocelot.json
文件中,配置路由和认证信息。假设所有请求通过 Ocelot 转发至下游 API,并要求 JWT 验证:{ "Routes": [ { "DownstreamPathTemplate": "/connect/token", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "UpstreamPathTemplate": "/connect/token", "UpstreamHttpMethod": [ "POST" ] }, { "DownstreamPathTemplate": "/api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/api/{everything}", "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ], "AuthenticationOptions": { "AuthenticationProviderKey": "Bearer", "AllowedScopes": [ "api" ] }, "AddHeadersToRequest": { "Authorization": "Authorization" } } ], "GlobalConfiguration": { "BaseUrl": "http://localhost:5000" } }
AddHeadersToRequest
和AuthenticationOptions
这两个配置项在Ocelot的路由规则中起着至关重要的作用,它们分别负责请求头的处理和认证授权的管理。AddHeadersToRequest
配置项允许你将上游请求(客户端发送到Ocelot网关的请求)的头部信息添加到转发到下游服务(实际处理请求的API服务)的请求中。在这个例子中,"Authorization": "Authorization"
表示将上游请求中的Authorization
头部原封不动地传递给下游服务。这通常用于传递认证信息,例如JWT token。当下游服务需要知道用户的身份验证信息时,这个配置就非常有用。通过传递Authorization
头部,下游服务可以根据token进行身份验证和授权。AuthenticationOptions
配置项则定义了Ocelot如何对上游请求进行认证和授权。"AuthenticationProviderKey": "Bearer"
指定了用于认证的方案是"Bearer",这通常与JWT认证一起使用。这意味着Ocelot会尝试使用"Bearer"认证方案来验证上游请求的身份。"AllowedScopes": ["api"]
定义了允许的OAuth 2.0 scopes。只有当上游请求携带的token包含"api"这个scope时,Ocelot才会允许该请求通过这个路由。Scope是一种权限声明,用于限制客户端可以访问的资源。通过配置AllowedScopes
,可以确保只有具有适当权限的客户端才能访问特定的API端点。如果请求的token不包含"api"这个scope,Ocelot会拒绝该请求,返回未经授权的错误。
3.3 业务服务开发
Weather项目使用的是带有示例的模板创建的,因此就不用编写控制器了,只用在控制器上加上[Authorize]
即可。
打开Program类,在该类中新增身份验证服务的代码,需要注意的是这里的IssuerSigningKey
需要和Gateway
中的配置一样:
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Convert.FromBase64String("SguGSpvaRLMwnmnxiBHRdSxRpBDSiD8+8J1qp1czuD8=")
),TokenDecryptionKey =
new SymmetricSecurityKey(
Convert.FromBase64String("na8LnVekSu5b3fgdUhyo+KuLTMVGYLtgHrTTpKCB5VY=")),
ValidateIssuer = false,
ValidIssuer = "http://localhost:5002",
ValidateAudience = false
};
});
这段代码和Gateway
类似,就不再讲解。看到这里大家一定很奇怪,为什么网关服务已经配置了身份验证,业务服务还要进行一次验证呢?其实要分情况的,如果项目是内网项目,无需访问外网就可使用的话就不需要给业务负责增加身份验证的代码,如果是外网项目就必须增加身份验证服务。
四、总结
本文详细介绍了微服务架构中如何通过JWT认证和IdentityServer实现统一的身份认证与授权,以提升系统安全性。首先,阐述了JWT的工作原理及其在Ocelot网关中的应用,强调了JWT的无状态、轻量和跨平台兼容性,并强调了密钥安全的重要性。其次,介绍了IdentityServer作为.NET的开源身份认证与授权框架,如何简化认证流程,支持多种认证流程,并与JWT结合实现安全令牌管理。最后,通过一个集成Ocelot、IdentityServer和JWT认证的案例演示,详细讲解了认证服务器、网关和业务服务的配置步骤,包括OpenIddict的配置、数据库迁移、种子数据设置以及令牌端点的实现。该方案通过在网关层实现身份认证、权限校验和数据加密,有效防范恶意攻击,构建可靠的访问环境,切实保护企业数据安全。