Minimal API数据库与授权系统快速入门

Minimal API数据库与授权系统快速入门

设置Scalar

创建web api项目,从.net9开始移除了默认对SWagger的支持,建议使用Scalar,直接安装nuget包:Scalar.AspNetCore

简单使用只需要:

1
2
3
4
5
6
7
8
builder.Services.AddOpenApi();
if (app.Environment.IsDevelopment())
{
// 暴露 OpenAPI 文档端点,Scalar 会读取该文档渲染页面。
app.MapOpenApi();
// 映射 Scalar API Reference 页面,用于在浏览器中查看和调试接口。
app.MapScalarApiReference();
}

如果需要JWT等的自动认证可以这样设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 注册 OpenAPI 文档生成服务。
// Scalar 读取的接口文档本质上来自 OpenAPI,因此这里先把 OpenAPI 服务加入依赖注入容器。
// 后续 app.MapOpenApi() 会把这里生成的文档暴露成 HTTP 端点,Scalar 页面再基于该文档渲染接口调试 UI。
builder.Services.AddOpenApi(opt =>
{
// DocumentTransformer 可以在 OpenAPI 文档最终输出前,对文档内容进行统一加工。
// 这里主要用于给整份接口文档追加 JWT Bearer 安全定义,让 Scalar 知道当前 API 支持通过 Authorization 请求头传递 JWT。
opt.AddDocumentTransformer((document, context, cancellationToken) =>
{
// 1. 确保 OpenAPI 文档的 Components、SecuritySchemes、Security 容器已初始化。
// Components 用来存放可复用的文档组件,例如安全方案、Schema、响应定义等。
// SecuritySchemes 用来声明“如何认证”,例如 Bearer Token、API Key、OAuth2 等。
// Security 用来声明“哪些安全方案应用到接口文档上”。
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Security ??= [];

// 2. 定义一个名为 Bearer 的 JWT 认证方式。
// Type = Http 表示这是 HTTP 标准认证方案。
// Scheme = bearer 表示请求头格式是 Authorization: Bearer {token}。
// BearerFormat = JWT 是给文档工具看的提示,告诉 Scalar 这里期望填入 JWT。
// Description 会展示在 Scalar 的认证输入框附近,用来提示调用者直接粘贴 token。
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "请输入 JWT Token,不需要手动输入 Bearer,直接粘贴 token 即可,无需空格。"
};

// 3. 把上面定义的 Bearer 安全方案应用到整份 OpenAPI 文档。
// 这样 Scalar 页面会在接口调试时支持统一填写 JWT,并自动把它带到 Authorization 请求头中。
// 注意:这只是“文档和调试页面”的安全声明,不等价于真正的服务端鉴权。
// 真正的认证和授权仍然由 AddAuthentication、AddAuthorization、UseAuthentication、UseAuthorization 负责。
document.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", document, null)] = []
});

return Task.CompletedTask;
});
});

为了项目打开后自动打开scalar的界面,打开launchSettings.json,对里面的内容进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "scalar",
"applicationUrl": "http://localhost:5204",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

配置数据库

  1. 使用ORM进行数据库的管理,要安装nuget包
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Tools
  • Pomelo.EntityFrameworkCore.MySql,这个根据需要,用什么数据库,就安装什么就可以
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore ,一般直接使用Identity框架
  1. 根据需要建立相关的Model类,要涉及到一对一、一对多、多对多的情况,根据需要进行配置
  2. 创建DataContext
1
2
3
4
5
6
7
8
9
//可以直接使用IdentityDbContext,他会自动管理用户鉴权,如果不需要可以直接使用DbContext
//IdentityDbContext是继承自DbContext
public class DataContext(DbContextOptions<DataContext> options) : IdentityDbContext<ApplicationUser>(options)
{
public DbSet<Article> Articles
{
get; set;
}
}
  1. 注入到容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从配置文件中读取数据库连接字符串。
// 默认会读取 appsettings.json / appsettings.Development.json 中 ConnectionStrings:DefaultConnection。这是mysql的链接字符串
//"ConnectionStrings": {
// "DefaultConnection": "server=localhost;port=3306;database=miniknowledge;user=root;password=aaaa;"
//}
var connectString = builder.Configuration.GetConnectionString("DefaultConnection");

// 注册 EF Core 的数据库上下文 DataContext。
// AddDbContext 会把 DataContext 加入依赖注入容器,生命周期默认为 Scoped,适合一次 HTTP 请求使用一个 DbContext。
// UseMySql 表示使用 MySQL/MariaDB 数据库提供程序。
// ServerVersion.AutoDetect(connectString) 会根据连接字符串连接数据库并自动检测服务器版本,便于 Pomelo/EF Core 生成兼容 SQL。
builder.Services.AddDbContext<DataContext>(opt =>
{
opt.UseMySql(connectString, ServerVersion.AutoDetect(connectString));
});
  1. 可以打开程序包管理控制台,执行add-migration 等迁移命令

配置授权与鉴权

  1. 注册 ASP.NET Core Identity 的核心服务
1
2
3
4
5
6
// ApplicationUser 是当前项目自定义的用户类型,继承自 IdentityUser。
// AddIdentityCore 只注册 Identity 的核心能力,例如 UserManager、密码哈希、用户验证等,不会自动启用完整 Cookie UI。
// AddRoles<IdentityRole>() 启用角色系统,让 UserManager 可以 AddToRoleAsync/GetRolesAsync,并注册 RoleManager。
builder.Services.AddIdentityCore<ApplicationUser>() //把我的定义的用户类注入
.AddRoles<IdentityRole>() //启用角色系统的时候使用
.AddEntityFrameworkStores<DataContext>();//放到哪个数据库
  1. 注测认证,如果用JWT,需要安装nuget包Microsoft.AspNetCore.Authentication.JwtBearer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 从配置读取 JWT 参数。
// Jwt:Key 是服务端签名密钥,必须保密;客户端不能知道这个值。
// Jwt:Issuer 表示 token 签发方,通常是当前 API 服务名称或域名。
// Jwt:Audience 表示 token 接收方,通常是当前 API 或前端客户端标识。
// 登录接口签发 JWT 时使用这些值;认证中间件验证 JWT 时也会使用同一组值进行校验。
// "JWT": {
// "Issuer": "MiniKnowledge",
// "Audience": "MiniKnowledge",
// "Key": "MiniKnowledge_Development_Secret_Key_At_Least_32_Characters"
// }
var jwtKey = builder.Configuration["Jwt:Key"]!;
var jwtIssuer = builder.Configuration["Jwt:Issuer"]!;
var jwtAudience = builder.Configuration["Jwt:Audience"]!;

// 注册认证服务,并把默认认证方案设置为 JWT Bearer。
// 认证 Authentication 负责回答“这个请求是谁发来的”。
// 当请求携带 Authorization: Bearer {token} 时,JwtBearer 中间件会解析 token,并把解析后的 ClaimsPrincipal 设置到 HttpContext.User。
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
// TokenValidationParameters 定义服务端收到 JWT 后应该如何验证它。
// 只要其中任何关键验证失败,例如签名不正确、issuer 不匹配、audience 不匹配或 token 过期,当前请求就不会被视为已认证。
opt.TokenValidationParameters = new TokenValidationParameters
{
// 验证 token 的签发方 iss。
// 只有 token 内的 iss 与 ValidIssuer 一致时才认为签发方可信。
ValidateIssuer = true,
ValidIssuer = jwtIssuer,

// 验证 token 的接收方 aud。
// 只有 token 内的 aud 与 ValidAudience 一致时,才认为这个 token 是发给当前 API 使用的。
ValidateAudience = true,
ValidAudience = jwtAudience,

// 验证 JWT 签名。
// 这是 JWT 安全性的核心:客户端可以看到 token 内容,但不能在没有服务端密钥的情况下伪造合法签名。
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),

// 验证 token 是否过期。
// 登录接口签发 token 时设置 expires,JwtBearer 会根据 exp 字段判断 token 是否仍然有效。
ValidateLifetime = true,

// 允许服务器之间存在少量时间误差。
// 这里设置为 2 分钟,表示 token 到期前后会有 2 分钟容忍范围;如果希望严格过期,可改为 TimeSpan.Zero。
ClockSkew = TimeSpan.FromMinutes(2),

// 指定 ClaimsPrincipal.Identity.Name 从哪个 Claim 中读取。
// 如果登录签发 JWT 时写入 ClaimTypes.Name,那么后续 User.Identity.Name 就会得到该值。
NameClaimType = ClaimTypes.Name,

// 指定角色授权从哪个 Claim 中读取。
// RequireRole、User.IsInRole 都会根据 RoleClaimType 查找角色 Claim。
// 因此登录接口需要把用户角色写成 ClaimTypes.Role,角色鉴权才能生效。
RoleClaimType = ClaimTypes.Role
};
});

  1. 注册授权,有时候需要对不同角色等进行权限控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 授权 Authorization 负责回答“这个已经认证的用户能不能访问某个资源”。
// 这里定义了两个策略,策略名直接复用了 AppRoles 中的角色常量:
// - admin 策略:要求用户拥有 admin 角色。
// - user 策略:要求用户拥有 user 或 admin 角色,因此管理员也可以访问普通用户资源。
// 注意:RequireAuthorization(AppRoles.Admin) 传入的是“策略名”,不是直接传角色名;真正的角色判断发生在 p.RequireRole(...) 中。
builder.Services.AddAuthorization(config =>
{
config.AddPolicy(AppRoles.Admin, p =>
{
// 访问使用 admin 策略的接口时,当前 JWT 中必须存在符合 RoleClaimType 的 admin 角色 Claim。
p.RequireRole(AppRoles.Admin);
});
config.AddPolicy(AppRoles.User, p =>
{
// 访问使用 user 策略的接口时,user 和 admin 任意一个角色满足即可。
// RequireRole 传多个角色是“或”的关系,不是“且”的关系。
p.RequireRole(AppRoles.User, AppRoles.Admin);
});
});

  1. 把认证和授权加入到管道,注意顺序
1
2
3
4
5
6
7
// 把认证中间件加入 HTTP 请求处理管道。
// 必须放在 UseAuthorization 之前,因为授权需要依赖认证解析出的 HttpContext.User。
app.UseAuthentication();

// 把授权中间件加入 HTTP 请求处理管道。
// 当端点上配置 RequireAuthorization 时,请求会在这里根据策略/角色/登录状态决定是否允许继续执行。
app.UseAuthorization();

注册和登录

配置好上文后,下面实现注册和登录

  • 注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//创建 /api/auth 路由分组
// 分组可以给同一模块下的接口统一添加前缀、Tag、授权规则、过滤器等。
var group = router.MapGroup("/api/auth");

// 注册接口:POST /api/auth/register。
// 客户端提交用户名、密码、确认密码和角色后,服务端创建 Identity 用户,并把用户加入指定角色。
// WithDisplayName 主要是端点显示名;WithSummary 主要是显示名称和 WithTags 会显示分组。
group.MapPost("/register", Register)
.WithDisplayName(nameof(Register))
.WithSummary("注册")
.WithTags("注册Tag");

// 用户注册处理方法。
// RegisterRequest 由请求体 JSON 自动绑定而来,并会根据模型上的 DataAnnotations 执行验证。
// UserManager<ApplicationUser> 由 Identity 注入,用于创建用户、设置密码、把用户加入角色等。
// RoleManager<IdentityRole> 由 AddRoles<IdentityRole>() 注入,用于创建和查询角色。
// 返回 Results<Ok, BadRequest<IEnumerable<IdentityError>>> 表示该接口只会返回 200 OK 或 400 BadRequest 两类结果。
static async Task<Results<Ok, BadRequest<IEnumerable<IdentityError>>>> Register(Model.RegisterRequest register, UserManager<ApplicationUser> userManager,RoleManager<IdentityRole> roleManager)
{
// 创建 Identity 用户实体。
// 当前只设置 UserName,暂时没有设置 Email、PhoneNumber 等字段。
// 密码不会直接保存到实体属性中,而是由 userManager.CreateAsync 进行哈希后写入 PasswordHash。
var user = new ApplicationUser
{
UserName = register.UserName,
};

// 创建用户并写入数据库。
// Identity 会自动完成密码强度校验、用户名校验、密码哈希、用户记录保存等操作。
// 如果密码不符合规则、用户名重复或数据库写入失败,result.Succeeded 会是 false。
var result = await userManager.CreateAsync(user, register.Password);
if (!result.Succeeded)
{
// 把 Identity 返回的错误集合直接返回给客户端。
// 常见错误包括密码过短、密码缺少数字、用户名已存在等。
return TypedResults.BadRequest(result.Errors);
}

// 检查客户端请求的角色是否已经存在。
// AddToRoleAsync 不会自动创建角色;如果角色不存在,直接加入角色会失败。
// 当前逻辑选择在注册时自动创建不存在的角色,便于开发阶段测试。
// 生产环境通常不建议让客户端随意传角色并自动创建,否则可能产生越权风险。
if (!await roleManager.RoleExistsAsync(register.Role))
{
// 创建新的 Identity 角色。
// 角色会保存到 AspNetRoles 表中,后续用户角色关系会保存到 AspNetUserRoles 表中。
await roleManager.CreateAsync(new IdentityRole(register.Role));
}

// 把刚注册的用户加入指定角色。
// 这样用户登录时,Login 方法可以通过 GetRolesAsync 读取角色,并写入 JWT 的 role claims。
await userManager.AddToRoleAsync(user,register.Role);

// 注册和分配角色都完成后返回 200 OK。
return TypedResults.Ok();

}
  • 登录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 登录接口:POST /api/auth/login。
// 客户端提交用户名和密码,校验成功后返回 JWT 字符串。
// 后续请求需要把该 JWT 放入 Authorization: Bearer {token} 请求头。
group.MapPost("/login", Login)
.WithDisplayName(nameof(Login))
.WithTags("登录");


// 用户登录处理方法。
// LoginRequest 来自请求体,包含用户名和密码。
// UserManager 用于查找用户、校验密码、读取用户角色。
// IConfiguration 用于读取 appsettings.json 中的 Jwt:Key、Jwt:Issuer、Jwt:Audience 配置。
// 登录成功返回 JWT 字符串;用户名不存在或密码错误时返回 401 Unauthorized。
static async Task<Results<Ok<string>,UnauthorizedHttpResult>> Login(Model.LoginRequest request,UserManager<ApplicationUser> userManager,IConfiguration configuration)
{
// 根据用户名查找用户。
// FindByNameAsync 查询的是 Identity 标准化后的用户名字段,具体实现由 EF Core Identity Store 完成。
var user =await userManager.FindByNameAsync(request.UserName);
if (user is null)
{
// 用户不存在时返回 401,而不是 404。
// 这样可以避免向外暴露“某个用户名是否存在”的信息。
return TypedResults.Unauthorized();
}

// 校验用户输入的明文密码是否与数据库中的 PasswordHash 匹配。
// Identity 内部会使用配置的密码哈希算法进行验证,不需要手动比较密码。
var pwdValid = await userManager.CheckPasswordAsync(user, request.Password);
if (!pwdValid)
{
// 密码错误同样返回 401,保持和用户不存在一致的响应,减少账号枚举风险。
return TypedResults.Unauthorized();
}

// 构建 JWT 中要携带的 Claims。
// Claim 是 JWT 的声明信息,表示当前用户的身份属性,例如用户 ID、用户名、角色等。
// NameIdentifier 通常用于保存用户唯一 ID,后续做“只能修改自己的资源”这类授权时非常常用。
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier,user.Id),
};

//注意,想要后面用到什么样的claim,就必须加入,否则后面读取不到

// 从 Identity 读取该用户拥有的所有角色。
// 这里依赖 Program.cs 中 AddRoles<IdentityRole>() 和 AddEntityFrameworkStores<DataContext>() 提供角色存储能力。
var roles = await userManager.GetRolesAsync(user);
foreach (var role in roles)
{
// 把每个角色写入 JWT。
// Program.cs 中 TokenValidationParameters.RoleClaimType = ClaimTypes.Role,
// 所以 RequireRole 和 User.IsInRole 会从这些 ClaimTypes.Role 声明中判断角色。
claims.Add(new Claim(ClaimTypes.Role, role));
}

// 从 appsettings.json 读取 JWT 配置
var jwtKey = configuration["Jwt:Key"]!;
var jwtIssuer = configuration["Jwt:Issuer"]!;
var jwtAudience = configuration["Jwt:Audience"]!;

// 使用配置中的 Jwt:Key 创建对称安全密钥。
// HMAC-SHA256 是对称签名算法,签发和验证 token 使用同一个密钥。
// 该密钥必须足够长且不能泄露,否则攻击者可以伪造合法 JWT。
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));

// 创建签名凭据。
// SigningCredentials 告诉 JwtSecurityTokenHandler 应该用哪个密钥、哪种算法给 JWT 签名。
var credentials =new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

// 创建 JWT 对象。
// issuer/audience 会写入 token 的 iss/aud 字段,并在请求进入时由 JwtBearer 根据 Program.cs 的配置进行校验。
// claims 会写入 token payload,后续服务端会把它们解析成 ClaimsPrincipal。
// expires 控制 token 有效期;这里设置为 2 小时。
// signingCredentials 负责生成 token 签名,防止 token 被客户端篡改。
var jwtToken = new JwtSecurityToken(
issuer:jwtIssuer,
audience:jwtAudience,
claims:claims,
expires:DateTime.UtcNow.AddHours(2),
signingCredentials: credentials
);

// 把 JwtSecurityToken 对象序列化成客户端可以携带的紧凑字符串。
// 客户端拿到该字符串后,需要在请求头中按 Authorization: Bearer {token} 的格式发送。
string token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
return TypedResults.Ok(token);
}
  • 鉴权用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 管理员测试接口:GET /api/auth/adminonly。
// RequireAuthorization(AppRoles.Admin) 这里传入的是授权策略名。
// Program.cs 中定义了同名策略,并在策略里要求用户拥有 admin 角色。
// 如果 JWT 有效但不包含 admin 角色,会返回 403 Forbidden。
group.MapGet("adminonly", () =>
{
return TypedResults.Ok("我是管理员");
})
.WithTags("我是管理员")
.RequireAuthorization(AppRoles.Admin);

// 普通用户测试接口:GET /api/auth/useronly。
// RequireAuthorization(AppRoles.User) 使用 Program.cs 中定义的 user 策略。
// 当前 user 策略允许 user 或 admin 角色访问,因此普通用户和管理员都可以访问该接口。
group.MapGet("useronly", () =>
{
return TypedResults.Ok("我是注册用户");
})
.WithTags("我是注册用户")
.RequireAuthorization(AppRoles.User);

Minimal API数据库与授权系统快速入门

https://bubuweiying.site/MinimalAPI快速入门/

作者

步步为营

发布于

2026-06-26

更新于

2026-08-07

许可协议