实用指南:24.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--认证微服务

news/2025/10/5 10:58:47/文章来源:https://www.cnblogs.com/tlnshuju/p/19126376

实用指南:24.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--认证微服务

2025-10-05 10:55  tlnshuju  阅读(0)  评论(0)    收藏  举报

SP.IdentityService 项目为微服务架构中的核心认证中心,采用 OpenIddict 框架实现 OAuth2.0 和 OpenID Connect 协议,提供完整的身份认证和授权解决方案。项目集成了 ASP.NET Core Identity 框架,实现了用户管理、角色权限控制等基础功能,并通过 Entity Framework Core 与 MySQL 数据库进行数据持久化。在安全方面,项目实现了严格的密码策略、账户锁定机制,并支持 Token 认证和刷新令牌机制。作为微服务架构的一部分,项目使用 Nacos 进行服务注册与发现,并集成了 Redis 缓存和 RabbitMQ 消息队列,提升了系统性能和可靠性。同时,项目提供了完整的 Swagger API 文档,支持 OAuth2.0 认证流程,方便开发人员进行接口调试和集成。项目采用模块化设计,包含用户管理、角色权限、消息服务等核心模块,并提供了邮件通知、消息队列等扩展功能。通过合理的代码组织和最佳实践,确保了系统的可维护性和可扩展性,为其他微服务提供统一的身份认证和授权服务。

从这篇文章开始,我们将逐步实现微服务架构中的身份认证服务。我们将使用 ASP.NET Core Identity 框架和 OpenIddict 库来实现 OAuth2.0 和 OpenID Connect 协议。以下是我们将要实现的功能用户认证和令牌管理用户信息管理角色管理权限管理 以及 邮件通知功能。这篇文章中我们一起实现用户认证和令牌管理

Tip:文章只讲解核心代码和实现思路。

一、OpenIddict服务扩展

SP.IdentityService 项目中,我们需要配置 OpenIddict 服务。我们将创建一个新的扩展方法 AddOpenIddict,用于配置 OpenIddict 服务。
在实现 OpenIddict 服务扩展时,我们首先需要定义一个扩展方法 AddOpenIddict,这样在 Program.cs 里就能直接通过 services.AddOpenIddict(configuration) 来注册相关服务了。这个扩展方法的参数包括 IServiceCollection(用于依赖注入)和 IConfiguration(用来读取配置信息,比如密钥等)。在方法内部,我们会先从配置文件(我们的项目的配置文件是在nacos中)里读取 JWT 的签名密钥和加密密钥,这里要注意的是这两个密钥都需要是 Base64 格式的字符串,否则后续配置会报错。代码如下:

using Microsoft.IdentityModel.Tokens
;
using OpenIddict.Abstractions
;
using OpenIddict.Validation.AspNetCore
;
using SP.IdentityService.DB
;
namespace SP.IdentityService
;
/// <summary>/// OpenIddict服务扩展
/// </summary>
public
static
class OpenIddictServiceExtensions
{
/// <summary>/// 添加OpenIddict服务
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public
static IServiceCollection AddOpenIddict(
this IServiceCollection services, IConfiguration configuration)
{
// 从配置文件中读取密钥
string signingKey = configuration["Jwt:SigningKey"]
;
string encryptionKey = configuration["Jwt:EncryptionKey"]
;
// more code ...
}
}

接下来就是 OpenIddict 的核心配置了。我们通过 services.AddOpenIddict() 开始链式调用,首先用 .AddCore 配置数据存储,这里选择 Entity Framework Core,并指定了 IdentityServerDbContext 作为上下文,这样所有 OpenIddict 相关的数据(比如授权、令牌等)都会存到数据库里。代码如下:

services.AddOpenIddict(
)
.AddCore(options =>
{
// 使用 EntityFrameworkCore作为数据源
options.UseEntityFrameworkCore(
)
.UseDbContext<IdentityServerDbContext>();})

然后是服务器端的配置部分。我们设置了令牌端点为 connect/token,并且支持密码模式、客户端凭证模式和刷新令牌模式,这样无论是用户登录还是服务间通信都能灵活支持。作用域方面,注册了 api 和离线访问(也就是刷新令牌),同时还注册了常用的声明,比如用户名、角色和邮箱。令牌的有效期也做了详细设置,访问令牌 30 分钟,刷新令牌 14 天,并且有 2 分钟的重用宽限期,防止并发刷新时出现问题。

安全方面,开发环境下我们用的是临时证书(AddDevelopmentEncryptionCertificateAddDevelopmentSigningCertificate),但生产环境一定要换成正式证书。签名和加密密钥则是用配置文件里的对称密钥(Base64 格式),分别通过 AddSigningKeyAddEncryptionKey 加入。这里还允许了匿名客户端,也就是说 client_id 和 client_secret 可以不传,这在内部服务间通信时很方便,但如果是对外的应用就要谨慎使用。我们还启用了引用刷新令牌(令牌本身只是一个引用,实际内容存在服务端,更安全),并且关闭了访问令牌加密(这样令牌内容可见,调试更方便,生产环境可以根据需要开启)。最后,集成了 ASP.NET Core,允许 HTTP(开发环境下),并启用了令牌端点直通,方便调试。代码如下:

.AddServer(options =>
{
// 设置令牌端点
options.SetTokenEndpointUris("connect/token"
)
;
// 启用密码模式
options.AllowPasswordFlow(
) // 开启密码模式
.AllowClientCredentialsFlow(
) // 开启客户端令牌模式
.AllowRefreshTokenFlow(
)
;
// 开启刷新令牌
// 注册授权范围
options.RegisterScopes("api"
, OpenIddictConstants.Scopes.OfflineAccess)
;
// 注册所有资源
options.RegisterClaims(
OpenIddictConstants.Claims.Name,
OpenIddictConstants.Claims.Role,
OpenIddictConstants.Claims.Email)
;
// 配置令牌属性
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30
)
)
;
options.SetRefreshTokenLifetime(TimeSpan.FromDays(14
)
)
;
options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(2
)
)
;
// 使用开发环境下的临时密钥(生产环境请使用持久化证书)
options.AddDevelopmentEncryptionCertificate(
)
.AddDevelopmentSigningCertificate(
)
;
options.AddSigningKey(
new SymmetricSecurityKey(
Convert.FromBase64String(signingKey)
)
)
;
options.AddEncryptionKey(
new SymmetricSecurityKey(Convert.FromBase64String(encryptionKey)
)
)
;
// 允许接收表单数据
options.AcceptAnonymousClients(
)
;
// 配置令牌选项 - 使用引用刷新令牌使令牌更短
options.UseReferenceRefreshTokens(
)
;
options.DisableAccessTokenEncryption(
)
;
// 集成 ASP.NET Core
options.UseAspNetCore(
)
.EnableTokenEndpointPassthrough(
)
.DisableTransportSecurityRequirement(
)
;
// 开发模式下禁用HTTPS
}
)

验证部分也很简单,直接用 .AddValidation 配置,指定使用本地 OpenIddict 服务器和 ASP.NET Core 集成,这样后续的令牌验证就都走本地服务了。最后别忘了注册认证方案,直接用 services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme),这样在控制器或者中间件里就能用 [Authorize] 进行权限控制了。代码如下:

.AddValidation(options =>
{
// 使用本地服务器进行验证
options.UseLocalServer(
)
;
// 使用 AspNetCore 进行验证
options.UseAspNetCore(
)
;
}
)
;
services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)
;
return services;

这一套配置下来,OpenIddict 就能支持多种 OAuth2 授权模式,非常适合微服务架构下的认证服务。但是需要注意的是,开发环境下允许 HTTP 和用开发证书没问题,但上线后一定要加强安全配置,比如强制 HTTPS、换成正式证书、密钥管理等。另外,密钥一定要用 Base64 格式,开发证书只适合本地调试,允许匿名客户端也只建议在内部服务间使用,外部应用要严格校验身份。

二、Program.cs配置

接下来,我们需要在 Program.cs 文件中配置 ASP.NET Core Identity 和 OpenIddict 服务。我们将使用 AddIdentity 方法来添加身份服务,并配置密码策略、锁定设置等。同时,我们还需要注册 OpenIddict 服务,以便实现 OAuth2.0 和 OpenID Connect 协议,代码如下:

// 添加ASP.NET Core Identity服务
builder.Services.AddIdentity<SpUser, SpRole>(options =>{// 密码策略配置options.Password.RequireDigit = true;// 要求数字options.Password.RequireLowercase = true;// 要求小写字母options.Password.RequireUppercase = true;// 要求大写字母options.Password.RequireNonAlphanumeric = true;// 要求特殊字符options.Password.RequiredLength = 6;// 最小长度options.Password.RequiredUniqueChars = 4;// 要求不同字符的最小数量// 锁定设置options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);// 锁定15分钟options.Lockout.MaxFailedAccessAttempts = 5;// 5次失败尝试后锁定options.Lockout.AllowedForNewUsers = true;// 对新用户启用锁定// 禁用用户名和邮箱规范化options.User.RequireUniqueEmail = false;options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";}).AddEntityFrameworkStores<IdentityServerDbContext>().AddDefaultTokenProviders();// 替换默认的 UserStorebuilder.Services.AddScoped<IUserStore<SpUser>, SPUserStore>();builder.Services.AddOpenIddict(builder.Configuration);

这段代码首先通过 builder.Services.AddIdentity<SpUser, SpRole>(...) 这行代码将身份服务注册到依赖注入容器中,指定了用户类型 SpUser 和角色类型 SpRole。在配置委托中,详细设置了密码策略,密码必须包含数字、小写字母、大写字母、特殊字符,最小长度为 6,并且至少包含 4 个不同的字符,这样可以提升账户安全性。接着,配置了账户锁定策略:如果用户连续 5 次登录失败,账户会被锁定 15 分钟,并且新用户也适用这个锁定策略。options.User.RequireUniqueEmail = false 表示不强制邮箱唯一,AllowedUserNameCharacters 则限制了用户名只能包含指定的字符集。

随后 .AddEntityFrameworkStores<IdentityServerDbContext>() 指定了 Identity 使用的数据库上下文,这样用户和角色等信息会存储在 IdentityServerDbContext 管理的数据库中。.AddDefaultTokenProviders() 则为身份系统添加了默认的令牌生成器,比如用于密码重置、邮箱确认等场景。最后 builder.Services.AddOpenIddict(builder.Configuration); 这行代码将 OpenIddict 服务注册到容器中,并通过配置文件进行初始化。

三、配置数据库上下文

接下来,我们需要配置数据库上下文 IdentityServerDbContext,它将用于存储用户、角色和 OpenIddict 的相关数据。我们将创建一个新的类 IdentityServerDbContext,继承自 IdentityDbContext<SpUser, SpRole>,并实现 OpenIddict 的数据存储接口。代码如下:

using Microsoft.AspNetCore.Identity
;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore
;
using Microsoft.EntityFrameworkCore
;
using SP.Common
;
using SP.IdentityService.Models.Entity
;
namespace SP.IdentityService.DB
;
/// <summary>/// IdentityServer数据库上下文
/// </summary>
public
class IdentityServerDbContext : IdentityDbContext<SpUser, SpRole,
long>
{
/// <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(
)
;
// 修改Users表
modelBuilder.Entity<SpUser>(b =>{b.Property(x => x.UserName).IsRequired().HasMaxLength(50);b.Property(x => x.Email).HasMaxLength(100);b.Property(x => x.LockoutEnd);b.Property(x => x.PasswordHash).IsRequired();b.Ignore(x => x.NormalizedUserName);b.Ignore(x => x.NormalizedEmail);b.Ignore(x => x.SecurityStamp);b.Ignore(x => x.ConcurrencyStamp);b.Ignore(x => x.TwoFactorEnabled);b.Ignore(x => x.PhoneNumberConfirmed);b.Ignore(x => x.AccessFailedCount);});SeedData(modelBuilder);}/// <summary>/// 数据库连接配置/// </summary>
/// <param name="optionsBuilder"></param>protectedoverride void OnConfiguring(DbContextOptionsBuilder optionsBuilder){var serverVersion = ServerVersion.AutoDetect(_dbConfig.GetConnectionString("MySQLConnection"));optionsBuilder.UseMySql(_dbConfig.GetConnectionString("MySQLConnection"), serverVersion);}private void SeedData(ModelBuilder builder){// 添加默认角色SpRole adminRole =new SpRole {Id = Snow.GetId(), Name = "Admin", NormalizedName = "ADMIN"};SpRole userRole =new SpRole {Id = Snow.GetId(), Name = "User", NormalizedName = "USER"};builder.Entity<SpRole>().HasData(adminRole,userRole);// 添加默认用户var hasher =new PasswordHasher<SpUser>();SpUser adminUser =new SpUser{Id = Snow.GetId(),UserName = "admin",Email = "494324190@qq.com",EmailConfirmed = true,PasswordHash = hasher.HashPassword(null, "123*asdasd")};builder.Entity<SpUser>().HasData(adminUser);// 添加用户角色builder.Entity<IdentityUserRole<long>>().HasData(new IdentityUserRole<long>{UserId = adminUser.Id,RoleId = adminRole.Id});}}

这段代码定义了一个名为 IdentityServerDbContext 的类,它继承自 IdentityDbContext<SpUser, SpRole, long>,用于管理和操作与身份认证相关的数据表。这个类主要服务于基于 ASP.NET Core Identity 和 OpenIddict 的认证微服务,负责与数据库进行交互。构造函数通过依赖注入的方式接收 IConfiguration,用于获取数据库连接字符串,这样可以灵活地根据不同环境配置数据库连接。

OnConfiguring 方法中,代码通过 _dbConfig.GetConnectionString("MySQLConnection") 获取名为 MySQLConnection 的连接字符串,并利用 ServerVersion.AutoDetect 自动检测 MySQL 服务器版本,最后调用 optionsBuilder.UseMySql 配置 EF Core 使用 MySQL 作为数据库。这种写法让数据库配置更加灵活,便于后续维护和环境切换。

OnModelCreating 方法是 Entity Framework Core 的一个重要扩展点,用于自定义数据模型的结构。首先调用 base.OnModelCreating(modelBuilder) 保证父类的默认配置被应用。接着,modelBuilder.UseOpenIddict() 这行代码集成了 OpenIddict 的数据模型,确保认证相关表结构被正确创建。随后对 SpUser 实体进行了详细配置,将 UserName 字段设置为必填且最大长度为 50,Email 最大长度为 100,PasswordHash 也被标记为必填。通过 b.Ignore 忽略了一些不需要映射到数据库的属性,比如 NormalizedUserNameSecurityStamp 等,这样可以减少表结构冗余,只保留业务需要的字段。

SeedData 方法中,代码实现了数据库的初始化种子数据。首先创建了两个默认角色 AdminUser,并通过 builder.Entity<SpRole>().HasData 方法插入到数据库。然后使用 PasswordHasher<SpUser> 对象对默认管理员用户的密码进行加密,创建一个用户名为 admin 的用户,并设置邮箱、邮箱确认等属性,最后同样通过 HasData 方法插入数据库。最后,代码还为管理员用户分配了管理员角色,确保系统初始化后就有一个可用的管理员账号和角色权限。

这个类不仅负责数据库的连接和模型配置,还通过种子数据保证了系统的可用性和安全性。通过继承和重写 EF Core 的相关方法,实现了高度自定义和灵活的身份认证数据管理,适合微服务架构下的认证服务需求。

四、配置用户存储

在微服务架构中,我们需要自定义用户存储,以便更好地管理用户数据。我们将创建一个新的类 SPUserStore,实现 IUserStore<SpUser> 接口,并提供用户的增删改查等操作。代码如下:

using Microsoft.AspNetCore.Identity
;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore
;
using Microsoft.EntityFrameworkCore
;
using SP.IdentityService.Models.Entity
;
namespace SP.IdentityService.DB
;
/// <summary>/// 用户存储类
/// </summary>
public
class SPUserStore : UserStore<SpUser, SpRole, IdentityServerDbContext,
long>
{
/// <summary>/// 用户存储类构造函数
/// </summary>
/// <param name="context"></param>
/// <param name="describer"></param>
public SPUserStore(IdentityServerDbContext context, IdentityErrorDescriber describer =
null
)
:
base(context, describer)
{
}
/// <summary>/// 查找用户
/// </summary>
/// <param name="normalizedUserName"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public
override Task<SpUser> FindByNameAsync(string normalizedUserName,CancellationToken cancellationToken =default){return Users.FirstOrDefaultAsync(u => u.UserName == normalizedUserName, cancellationToken);}/// <summary>/// 查找用户通过邮箱/// </summary>
/// <param name="normalizedEmail"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>publicoverride Task<SpUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken =default){return Users.FirstOrDefaultAsync(u => u.Email == normalizedEmail, cancellationToken);}}

这段代码实现了一个自定义的用户存储类 SPUserStore,它主要用于在 ASP.NET Core Identity 框架下管理用户数据。SPUserStore 继承自 UserStore<SpUser, SpRole, IdentityServerDbContext, long>,它基于泛型参数指定了用户类型为 SpUser,角色类型为 SpRole,数据库上下文为 IdentityServerDbContext,主键类型为 long。通过继承 UserStoreSPUserStore 自动获得了许多用户管理的基础功能,比如用户的创建、删除、更新、查找等操作,无需手动实现所有接口方法。

在构造函数中,SPUserStore 接收一个 IdentityServerDbContext 实例作为参数,这个上下文类通常继承自 IdentityDbContext,用于与数据库进行交互。可选的 IdentityErrorDescriber 参数允许自定义错误信息的描述方式,便于本地化或个性化错误提示。构造函数内部直接调用了基类的构造函数,将参数传递下去,确保父类能够正常初始化。

SPUserStore 还重写了两个查找用户的方法。第一个是 FindByNameAsync,它根据标准化后的用户名normalizedUserName 异步查找用户。这里通过 Users.FirstOrDefaultAsync 查询数据库中的用户集合,查找用户名等于传入参数的用户。需要注意的是,这里直接用 u.UserName == normalizedUserName 进行比较,是因为我们前面将NormalizedUserName列屏蔽掉了。第二个方法 FindByEmailAsync 的实现方式类似,根据标准化后的邮箱查找用户,同样也是因为我们前面将NormalizedEmail屏蔽掉了。

通过自定义 SPUserStore,你可以灵活扩展用户存储逻辑,比如增加自定义的查找条件、支持多租户、集成第三方数据源等。整体来看,这段代码为微服务架构下的认证服务提供了基础的数据访问层,既复用了 Identity 框架的能力,又为后续扩展留出了空间。

五、编写授权控制器

在微服务架构中,我们需要一个控制器来处理用户认证和令牌管理。我们将创建一个新的控制器 AuthorizationController,用于处理用户登录、获取令牌等操作。代码如下:

using Microsoft.AspNetCore
;
using Microsoft.AspNetCore.Authentication
;
using Microsoft.AspNetCore.Identity.Data
;
using Microsoft.AspNetCore.Mvc
;
using OpenIddict.Abstractions
;
using OpenIddict.Server.AspNetCore
;
using SP.Common.ExceptionHandling.Exceptions
;
using SP.IdentityService.Models.Request
;
using SP.IdentityService.Service
;
namespace SP.IdentityService.Controllers
;
/// <summary>/// 授权控制器
/// </summary>
[Route("connect"
)]
[ApiController]
public
class AuthorizationController : ControllerBase
{
private
readonly IAuthorizationService _authorizationService;
private
readonly ILogger<AuthorizationController> _logger;/// <summary>/// 授权控制器构造函数/// </summary>
/// <param name="authorizationService"></param>
/// <param name="logger"></param>public AuthorizationController(IAuthorizationService authorizationService, ILogger<AuthorizationController> logger){_logger = logger;_authorizationService = authorizationService;}/// <summary>/// 令牌端点 - 获取访问令牌/// </summary>/// <remarks>/// 请求示例:/// /// POST /connect/token/// Content-Type: application/x-www-form-urlencoded/// /// grant_type=password&amp;username=admin&amp;password=123*asdasd&amp;scope=api offline_access////// 或者刷新令牌:/// /// grant_type=refresh_token&amp;refresh_token=YOUR_REFRESH_TOKEN&amp;scope=api/// /// 或者客户端凭证模式:/// /// grant_type=client_credentials&amp;client_id=YOUR_CLIENT_ID&amp;client_secret=YOUR_CLIENT_SECRET&amp;scope=api////// 注意:/// 1. 必须使用表单(form-data)方式提交,Content-Type为application/x-www-form-urlencoded/// 2. 不要将参数放在URL查询字符串中/// 3. 在刷新令牌模式下,refresh_token必须放在请求体中,不能放在URL中/// 4. 客户端凭证模式适用于服务器到服务器的API调用,不关联特定用户/// </remarks>/// <returns>返回访问令牌信息</returns>/// <response code="200">返回访问令牌</response>/// <response code="400">请求格式不正确或不支持的授权类型</response>/// <response code="403">认证失败</response>[HttpPost("token")][Consumes("application/x-www-form-urlencoded")][Produces("application/json")][ProducesResponseType(StatusCodes.Status200OK)][ProducesResponseType(StatusCodes.Status400BadRequest)][ProducesResponseType(StatusCodes.Status403Forbidden)]publicasync Task<ActionResult> Token(){// 检查是否通过查询参数传递敏感信息if (Request.Query.Count >0 &&(Request.Query.ContainsKey("refresh_token") || Request.Query.ContainsKey("password") ||Request.Query.ContainsKey("client_secret"))){thrownew BadRequestException("不要在URL中包含敏感信息,请使用表单提交方式");}// 检查请求头中是否包含敏感信息if (Request.Headers.Any(h => h.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase) &&h.Value.ToString().Contains("Basic"))){// 记录警告日志,但允许继续处理,因为某些客户端可能使用Basic认证_logger.LogWarning("检测到使用Basic认证,建议改用表单提交方式");}var request = HttpContext.GetOpenIddictServerRequest();if (request ==null){thrownew BadRequestException("请求格式不正确,请使用表单(application/x-www-form-urlencoded)提交");}// 处理资源所有者密码模式if (request.IsPasswordGrantType()){// 验证用户名和密码if (string.IsNullOrEmpty(request.Username) ||string.IsNullOrEmpty(request.Password)){thrownew BusinessException("用户名或密码不能为空");}var principal =await _authorizationService.LoginByPasswordAsync(request.Username, request.Password,request.GetScopes());// 确保 SignIn 方法只在授权端点调用return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);}// 处理刷新令牌if (request.IsRefreshTokenGrantType()){// 从 request 上下文中获取之前存储的身份验证票据var principal =(await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme))?.Principal;var newPrincipal =await _authorizationService.RefreshTokenAsync(request.RefreshToken, request.GetScopes(), principal);return SignIn(newPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);}// 处理客户端凭证模式if (request.IsClientCredentialsGrantType()){var clientId = request.ClientId;if (string.IsNullOrEmpty(clientId)){thrownew BusinessException("client_id不能为空");}var principal =await _authorizationService.HandleClientCredentialsAsync(clientId, request.GetScopes());return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);}// 不支持的授权类型return BadRequest(new{error = OpenIddictConstants.Errors.UnsupportedGrantType,error_description = "不支持的授权类型。"});}/// <summary>/// 注册用户/// </summary>/// <param name="user"></param>[HttpPost("register")]publicasync Task<ActionResult<long>> Register([FromBody] UserAddRequest user){var result =await _authorizationService.AddUserAsync(user);return Ok(result);}/// <summary>/// 发送邮件/// </summary>/// <param name="email"></param>[HttpPost("emails/send")]publicasync Task<ActionResult> SendEmail([FromBody] SendEmailRequest email){await _authorizationService.SendEmailAsync(email);return Ok();}/// <summary>/// 绑定邮箱/// </summary>/// <param name="verifyCode"></param>[HttpPost("email/bind")]publicasync Task<ActionResult> BindEmail([FromBody] VerifyCodeRequest verifyCode){await _authorizationService.AddEmailAsync(verifyCode);return Ok();}/// <summary>/// 重置密码/// </summary>/// <param name="resetPasswordRequest"></param>[HttpPut("password/reset")]publicasync Task<ActionResult> ResetPassword([FromBody] ResetPasswordRequest resetPasswordRequest){await _authorizationService.ResetPasswordAsync(resetPasswordRequest);return Ok();}}

这段代码实现了一个名为 AuthorizationController 的控制器,主要用于处理用户认证和令牌管理相关的操作。控制器使用了 ASP.NET Core 的特性路由,通过 [Route("connect")] 定义了基础路由前缀,所有方法都以 /connect 开头。控制器的构造函数接收两个依赖项:IAuthorizationServiceILogger<AuthorizationController>IAuthorizationService 是一个自定义的服务接口,用于处理用户认证、令牌生成等逻辑;ILogger<AuthorizationController> 则用于记录日志。

控制器中定义了多个 API 端点,主要包括:

  1. Token:处理令牌端点的 POST 请求,支持多种授权类型,包括资源所有者密码模式、刷新令牌和客户端凭证模式。通过 HttpContext.GetOpenIddictServerRequest() 获取请求信息,并根据不同的授权类型调用相应的服务方法进行处理。返回的结果是一个身份验证票据,使用 SignIn 方法将其签名并返回给客户端。
  2. Register:处理用户注册的 POST 请求,接收一个 UserAddRequest 对象,调用 IAuthorizationService.AddUserAsync 方法进行用户注册,并返回新用户的 ID。
  3. SendEmail:处理发送邮件的 POST 请求,接收一个 SendEmailRequest 对象,调用 IAuthorizationService.SendEmailAsync 方法发送邮件。
  4. BindEmail:处理绑定邮箱的 POST 请求,接收一个 VerifyCodeRequest 对象,调用 IAuthorizationService.AddEmailAsync 方法进行邮箱绑定。
  5. ResetPassword:处理重置密码的 PUT 请求,接收一个 ResetPasswordRequest 对象,调用 IAuthorizationService.ResetPasswordAsync 方法进行密码重置。
    这些方法都使用了 ASP.NET Core 的特性来定义请求类型、响应类型和错误处理。比如,[Consumes("application/x-www-form-urlencoded")] 指定了请求的内容类型,[Produces("application/json")] 指定了响应的内容类型。每个方法还定义了不同的响应状态码,比如 200 OK、400 Bad Request 和 403 Forbidden 等。

这里我们重点讲解一下 Token 方法的实现。这个方法是处理令牌请求的核心逻辑,首先检查请求中是否包含敏感信息(如密码、刷新令牌等),如果有则抛出异常。接着,通过 HttpContext.GetOpenIddictServerRequest() 获取 OpenIddict 的请求对象,并根据授权类型进行不同的处理。
如果是资源所有者密码模式(Password Grant Type),则验证用户名和密码,并调用 IAuthorizationService.LoginByPasswordAsync 方法进行登录,最后返回签名的身份验证票据。如果是刷新令牌模式(Refresh Token Grant Type),则从请求中获取之前存储的身份验证票据,并调用 IAuthorizationService.RefreshTokenAsync 方法进行刷新,返回新的身份验证票据。如果是客户端凭证模式(Client Credentials Grant Type),则根据 client_id 调用 IAuthorizationService.HandleClientCredentialsAsync 方法处理。最后,如果请求的授权类型不被支持,则返回 400 Bad Request 错误。
通过这种方式,AuthorizationController 实现了一个完整的用户认证和令牌管理功能,支持多种授权模式,适用于微服务架构下的身份认证服务。

六、编写授权服务

在微服务架构中,我们需要一个服务来处理用户认证和令牌管理的业务逻辑。我们将创建一个新的服务 IAuthorizationService,用于处理用户登录、注册、令牌生成等操作,这里我们只看LoginByPasswordAsyncRefreshTokenAsyncHandleClientCredentialsAsync方法的实现。代码如下:

/// <summary>/// 密码登录
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="scopes"></param>
/// <returns></returns>
public
async Task<ClaimsPrincipal> LoginByPasswordAsync(string userName, string password,ImmutableArray<string> scopes){// 使用ASP.NET Core Identity验证用户SpUser? user =await _userManager.FindByNameAsync(userName);if (user ==null){thrownew BusinessException("用户不存在");}// 验证密码var result =await _signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true);if (!result.Succeeded){if (result.IsLockedOut){thrownew BusinessException("账户已锁定,请稍后再试。");}thrownew BusinessException("用户名或密码错误。");}// 创建用户身份并添加必要的声明var identity =new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);identity.AddClaim(OpenIddictConstants.Claims.Subject,await _userManager.GetUserIdAsync(user));identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName);identity.AddClaim(OpenIddictConstants.Claims.Email, user.Email);identity.AddClaim(OpenIddictConstants.Claims.Audience, "api");// 添加 aud 声明// 添加用户角色foreach (var role inawait _userManager.GetRolesAsync(user)){identity.AddClaim(ClaimTypes.Role, role);}// 创建 ClaimsPrincipal,并设置请求的范围var principal =new ClaimsPrincipal(identity);// 正确设置范围if (scopes.Any()){// 验证范围是否有效var validScopes =new[] {"api", OpenIddictConstants.Scopes.OfflineAccess};var filteredScopes = scopes.Intersect(validScopes).ToList();if (filteredScopes.Any()){principal.SetScopes(filteredScopes);}else{// 如果没有有效范围,默认设置为 apiprincipal.SetScopes("api");}}else{// 默认设置为 apiprincipal.SetScopes("api");}var roles =await _userManager.GetRolesAsync(user);// 根据用户角色或请求来源调整令牌生命周期if (roles.Contains("Admin")){// 管理员令牌生命周期较短principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(15));principal.SetRefreshTokenLifetime(TimeSpan.FromDays(7));}elseif (_httpContextAccessor?.HttpContext?.Request.Headers.TryGetValue("User-Agent",out var userAgent) ==true &&userAgent.ToString().Contains("Mobile")){// 移动设备令牌生命周期较长principal.SetAccessTokenLifetime(TimeSpan.FromHours(1));principal.SetRefreshTokenLifetime(TimeSpan.FromDays(30));}else{// 默认设置principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));principal.SetRefreshTokenLifetime(TimeSpan.FromDays(14));}return principal;}

LoginByPasswordAsync方法实现了基于用户名和密码的登录认证流程,通常用于用户通过账号密码登录系统时的场景。首先,它通过 _userManager.FindByNameAsync(userName) 查找用户对象 SpUser,如果找不到用户,则抛出用户不存在的业务异常。接下来,使用 _signInManager.CheckPasswordSignInAsync 验证密码是否正确,并且支持账户锁定机制。如果密码错误且账户被锁定,会抛出账户已锁定的异常,否则抛出用户名或密码错误。认证通过后,方法会创建一个 ClaimsIdentity,并为其添加一系列声明(Claims),包括用户ID(Subject)、用户名(Name)、邮箱(Email)和受众(Audience,通常用于标识令牌的目标API)。此外,还会遍历用户的角色列表,将每个角色作为 ClaimTypes.Role 添加到身份中。

然后,方法将 ClaimsIdentity 封装为 ClaimsPrincipal,并根据传入的 scopes 参数设置令牌的作用域。它会校验作用域是否合法(只允许 “api” 和 “offline_access”),如果没有合法作用域则默认设置为 “api”。接下来,根据用户角色和请求来源(比如是否为移动端),动态调整访问令牌和刷新令牌的有效期。例如,管理员的令牌有效期较短,移动端的有效期较长,普通用户则为默认值。最后返回构建好的 ClaimsPrincipal,用于后续生成令牌。

/// <summary>/// 刷新token
/// </summary>
/// <param name="refreshToken"></param>
/// <param name="scopes"></param>
/// <param name="principal"></param>
/// <returns></returns>
public
async Task<ClaimsPrincipal> RefreshTokenAsync(string? refreshToken, ImmutableArray<string> scopes,ClaimsPrincipal? principal){if (principal ==null){thrownew BusinessException("提供的刷新令牌无效或已过期");}// 检索用户身份var userId = principal.GetClaim(OpenIddictConstants.Claims.Subject);var user =await _userManager.FindByIdAsync(userId);if (user ==null){thrownew BusinessException("用户不存在");}// 如果用户被禁用或锁定,返回错误if (!await _userManager.IsEmailConfirmedAsync(user) ||await _userManager.IsLockedOutAsync(user)){thrownew BusinessException("用户已被禁用或锁定");}// 创建新的ClaimsPrincipalvar identity =new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);identity.AddClaim(OpenIddictConstants.Claims.Subject,await _userManager.GetUserIdAsync(user));identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName);identity.AddClaim(OpenIddictConstants.Claims.Email, user.Email);identity.AddClaim(OpenIddictConstants.Claims.Audience, "api");// 添加角色声明foreach (var role inawait _userManager.GetRolesAsync(user)){identity.AddClaim(ClaimTypes.Role, role);}var newPrincipal =new ClaimsPrincipal(identity);// 在设置新范围之前,保存原始令牌的离线访问范围bool hasOfflineAccess = principal.HasScope(OpenIddictConstants.Scopes.OfflineAccess);// 正确设置范围if (scopes.Any()){// 验证范围是否有效var validScopes =new[] {"api", OpenIddictConstants.Scopes.OfflineAccess};var filteredScopes = scopes.Intersect(validScopes).ToList();if (filteredScopes.Any()){newPrincipal.SetScopes(filteredScopes);}else{// 如果没有有效范围,默认设置为 apivar defaultScopes =new List<string> {"api"};// 如果原始令牌有离线访问范围,保留它if (hasOfflineAccess){defaultScopes.Add(OpenIddictConstants.Scopes.OfflineAccess);}newPrincipal.SetScopes(defaultScopes);}}else{// 默认设置为 apivar defaultScopes =new List<string> {"api"};// 如果原始令牌有离线访问范围,保留它if (hasOfflineAccess){defaultScopes.Add(OpenIddictConstants.Scopes.OfflineAccess);}newPrincipal.SetScopes(defaultScopes);}// 设置令牌生命周期newPrincipal.SetRefreshTokenLifetime(TimeSpan.FromDays(14));newPrincipal.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));return newPrincipal;}

RefreshTokenAsync这个方法用于刷新令牌(refresh token),即用户在 access token 过期后,通过 refresh token 换取新的 access token。首先会校验传入的 ClaimsPrincipal 是否有效,如果无效则抛出刷新令牌无效或已过期的异常。然后通过 principal.GetClaim(OpenIddictConstants.Claims.Subject) 获取用户ID,并查找用户对象。如果用户不存在,或者被禁用/锁定,则抛出相应的业务异常。

接下来,方法会重新构建一个新的 ClaimsIdentity,并为其添加用户ID、用户名、邮箱、受众等声明,以及用户的所有角色。然后创建新的 ClaimsPrincipal。在设置作用域时,方法会优先使用传入的 scopes,但如果没有合法作用域,则会保留原始令牌中的 “offline_access” 范围(如果原始令牌有的话),确保刷新令牌的能力不会丢失。否则,默认作用域为 “api”。最后为新的 principal 设置刷新令牌和访问令牌的有效期(分别为14天和30分钟),然后返回新的 ClaimsPrincipal,用于生成新的令牌。

/// <summary>/// 处理客户端凭证模式
/// </summary>
/// <param name="clientId"></param>
/// <param name="scopes"></param>
/// <returns></returns>
public
async Task<ClaimsPrincipal> HandleClientCredentialsAsync(string clientId, ImmutableArray<string> scopes){var application =await _applicationManager.FindByClientIdAsync(clientId) ??thrownew BuildAbortedException("找不到应用");// 创建一个新的ClaimsIdentity,包含将用于创建id_token、token或code的声明。var identity =new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType,OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);// 使用client_id作为主体标识符。identity.SetClaim(OpenIddictConstants.Claims.Subject,await _applicationManager.GetClientIdAsync(application));identity.SetClaim(OpenIddictConstants.Claims.Name,await _applicationManager.GetDisplayNameAsync(application));// 添加受众声明identity.SetClaim(OpenIddictConstants.Claims.Audience, "api");// 设置声明的目标identity.SetDestinations(static claim => claim.Type switch{// 当授予"profile"范围时(通过调用principal.SetScopes(...)),// 允许"name"声明同时存储在访问令牌和身份令牌中。OpenIddictConstants.Claims.Name when claim.Subject.HasScope(OpenIddictConstants.Permissions.Scopes.Profile)=>[OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken],// 否则,仅将声明存储在访问令牌中。_ =>[OpenIddictConstants.Destinations.AccessToken]});var principal =new ClaimsPrincipal(identity);// 正确设置范围if (scopes.Any()){// 验证范围是否有效var validScopes =new[] {"api"};var filteredScopes = scopes.Intersect(validScopes).ToList();if (filteredScopes.Any()){principal.SetScopes(filteredScopes);}else{// 如果没有有效范围,默认设置为 apiprincipal.SetScopes("api");}}else{// 默认设置为 apiprincipal.SetScopes("api");}// 设置令牌生命周期principal.SetAccessTokenLifetime(TimeSpan.FromHours(1));// 客户端凭证默认1小时有效期return principal;}

HandleClientCredentialsAsync这个方法实现了 OAuth2 的客户端凭证(Client Credentials)模式,主要用于服务与服务之间的认证(比如后端服务调用 API)。首先通过 _applicationManager.FindByClientIdAsync(clientId) 查找客户端应用信息,如果找不到则抛出异常。然后创建一个新的 ClaimsIdentity,并设置主体(Subject)为客户端ID,名称(Name)为客户端显示名,受众(Audience)为 “api”。方法还设置了声明的目标(Destination),比如当请求了 “profile” 范围时,“name” 声明会同时出现在访问令牌和身份令牌中,否则只出现在访问令牌中。接着,将 ClaimsIdentity 封装为 ClaimsPrincipal,并根据传入的 scopes 校验和设置作用域(只允许 “api”),如果没有合法作用域则默认设置为 “api”。最后将访问令牌的有效期设置为1小时,返回构建好的 ClaimsPrincipal

七、总结

通过以上步骤,我们实现了一个基于 ASP.NET Core Identity 和 OpenIddict 的认证微服务。这个微服务支持用户注册、登录、令牌生成和刷新等功能,并且可以灵活地扩展和定制。我们使用了 Entity Framework Core 作为数据访问层,MySQL 作为数据库存储用户和角色信息,同时通过自定义的用户存储类和授权服务,实现了对用户数据的高效管理。
ccessTokenLifetime(TimeSpan.FromHours(1)); // 客户端凭证默认1小时有效期
return principal;
}

`HandleClientCredentialsAsync`这个方法实现了 OAuth2 的客户端凭证(Client Credentials)模式,主要用于服务与服务之间的认证(比如后端服务调用 API)。首先通过 `_applicationManager.FindByClientIdAsync(clientId)` 查找客户端应用信息,如果找不到则抛出异常。然后创建一个新的 `ClaimsIdentity`,并设置主体(Subject)为客户端ID,名称(Name)为客户端显示名,受众(Audience)为 "api"。方法还设置了声明的目标(Destination),比如当请求了 "profile" 范围时,"name" 声明会同时出现在访问令牌和身份令牌中,否则只出现在访问令牌中。接着,将 `ClaimsIdentity` 封装为 `ClaimsPrincipal`,并根据传入的 `scopes` 校验和设置作用域(只允许 "api"),如果没有合法作用域则默认设置为 "api"。最后将访问令牌的有效期设置为1小时,返回构建好的 `ClaimsPrincipal`。
### 七、总结
通过以上步骤,我们实现了一个基于 ASP.NET Core Identity 和 OpenIddict 的认证微服务。这个微服务支持用户注册、登录、令牌生成和刷新等功能,并且可以灵活地扩展和定制。我们使用了 Entity Framework Core 作为数据访问层,MySQL 作为数据库存储用户和角色信息,同时通过自定义的用户存储类和授权服务,实现了对用户数据的高效管理。
在微服务架构中,这个认证服务可以作为其他服务的基础组件,提供统一的用户认证和授权功能。通过 OpenIddict,我们还可以轻松地集成 OAuth2 和 OpenID Connect 协议,为前端应用和第三方服务提供安全的访问控制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/928153.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详细介绍:STM32 串口通信①:USART 全面理解 + 代码详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

[算法/数据结构] 数据结构与算法

0 序工作这么些年,数据结构、及其基础算法,在工作实践中仍时长闪现。为此,此篇进行一定的归纳总结,并持续更新完善。1 概述:数据结构与算法简介数据结构(Data Structure)是计算机中存储、组织数据的方式。数据结…

基于点标注的弱监督目标检测方法研究 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

黑河北京网站建设设计公司的名字

亚马逊云科技近日在纽约峰会上宣布 Amazon Elastic Compute Cloud(EC2)P5 实例正式可用。这是一款下一代 GPU 实例&#xff0c;由最新的英伟达 H100 Tensor Core GPU 提供支持&#xff0c;可以满足客户在运行人工智能、机器学习和高性能计算工作负载时对高性能和高扩展性的需求…

怎么做网站app一流的聊城做网站公司

本文地址http://www.cnblogs.com/Bond/p/3972854.html 最近一直做移动端&#xff0c;没和IE6打交道了&#xff0c;瞬间感觉世界变美好了。移动端虽然还是各种坑&#xff0c;但是比起修复IE6那还是轻松多了&#xff0c;移动端很多效果可以用CSS3来做&#xff0c;感觉一切都和谐…

图论new

边双连通分量 #include<bits/stdc++.h> using namespace std; const int N = 5e5+5; int n, m, cnt, ans, dfn[N], low[N]; //dfn记录dfs序,low表示这个点除树边外能连到最浅 vector<int> mp[N], mp2[N]…

2025夹丝玻璃厂家最新企业品牌推荐排行榜,艺术夹丝玻璃,淋浴房夹丝玻璃,极简门夹丝玻璃,金属夹丝玻璃公司推荐!

在夹丝玻璃行业快速发展的当下,市场上的源头厂家数量不断增多,然而行业也面临着诸多问题。一方面,部分厂家缺乏核心技术,生产的夹丝玻璃在安全性、耐用性等方面难以满足市场需求,产品质量参差不齐;另一方面,一些…

斜率优化dp复习笔记

$$ f_j+sum_isum_j-sum_j^2>f_l+sum_isum_l-sum_l^2 \Rightarrow \frac{(f_j-sum_j^2)-(sum_l-sum_l^2)}{-sum_j-(-sum_l)}>sum_i $$那么点集就是 $(-sum_x,f_x-sum_x^2)$。那么对于当前点 $i$,所有斜率 $\leq …

掌握形式验证,护航芯片安全

在 IC 设计的世界里,任何一个微小错误都可能引发重大后果。形式验证(Formal Verification),以其数学证明的方式,成为确保设计可靠性与安全性的强大盾牌。 核心基础与优势解析 1、精准规范,明确预期行为 一切始于…

STL-list - 实践

STL-list - 实践2025-10-05 10:35 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-famil…

2025橡胶软接头厂家最新企业品牌推荐排行榜,法兰橡胶软接头,可曲挠,挠性,KXT,耐油,EPDM,耐腐蚀,三元乙丙橡胶软接头,橡胶柔性软接头公司推荐!

在橡胶软接头行业,质量管控与服务体系的缺失已成为制约产业升级的关键瓶颈。具体表现为: 产品质量两极分化:部分企业为压缩成本,违规采用再生胶、回收料等劣质原料,导致产品出现密封性失效、老化周期缩短等问题,…

整体二分笔记

整体二分 本来感觉挺神秘的一个东西, 学完了似乎没有多难, 放几个板子随便写写吧(今天数学不想做题) 从最最最最人尽皆知的区间第 \(k\) 大问题开始吧 引入 如果我想问你一个序列中的区间的第 \(k\) 大,你会如何?…

如何自做自己的网站网络设计开题报告

抽象节点这个特性自小程序基础库版本 1.9.6 开始支持。在组件中使用抽象节点有时&#xff0c;自定义组件模板中的一些节点&#xff0c;其对应的自定义组件不是由自定义组件本身确定的&#xff0c;而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。例如&am…

有什么网站可以做投票邯郸菜鸟网站建设

OD统一考试 题解&#xff1a; Java / Python / C 题目描述 一根X米长的树木&#xff0c;伐木工切割成不同长度的木材后进行交易&#xff0c;交易价格为每根木头长度的乘积。规定切割后的每根木头长度都为正整数,也可以不切割&#xff0c;直接拿整根树木进行交易。请问伐木工如…

响应网官方网站网站界面风格设计

1. 今日摸鱼计划 今天来学习一下ADC的原理&#xff0c;然后把ADC给实现 ADC芯片:ADC128S102 视频&#xff1a; 18A_基于SPI接口的ADC芯片功能和接口时序介绍_哔哩哔哩_bilibili 18B_使用线性序列机思路分析SPI接口的ADC芯片接口时序_哔哩哔哩_bilibili 18C_基于线性序列机的S…

量化投资 —— 实践

量化投资 —— 实践地址: https://item.taobao.com/item.htm?id=898078161839&mi_id=0000bSMU6-qva9mG_nEYyyLOcfGeJ5-tgwvwKtjY8IHE980&pvid=4580fb7a-c699-4f97-a5c0-8c810fa24035&scm=1007.40986.449…

详细介绍:性能优化 - 案例篇:缓存_Guava#LoadingCache设计

详细介绍:性能优化 - 案例篇:缓存_Guava#LoadingCache设计pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Conso…

2025年X射线管厂家最新企业品牌推荐排行榜,工业用金属陶瓷,波长色散荧光分析,应力衍射分析,管板角焊缝,轮胎检测,辐照,固定阳极波纹陶瓷,测厚,食品检测 X 射线管公司推荐

在工业无损检测领域,X 射线管作为核心元件,其质量与性能直接影响检测结果的准确性和可靠性,对国防、石油、电力、汽车零部件等关键行业的发展至关重要。当前,市场上 X 射线管厂家数量众多,产品质量参差不齐,部分…

AtCoder Beginner Contest 400

AT_abc400_d [ABC400D] Takahashi the Wall Breaker 一次踢两步也转移一下,直接搜 E - Ringos Favorite Numbers 3

网站托管服务方案网站建设办公软件销售技巧

目录 1、进程的虚拟内存分区与小于0x10000的小地址内存区 1.1、进程的虚拟内存分区 1.2、小于0x10000的小地址内存区 2、保存线程上下文的CONTEXT结构体 3、从汇编代码角度去理解多线程运行过程的典型实例 4、调用TerminateThread强制结束线程会导致线程中的资源没有释放…