写在前面
本文讲 ASP.NET Core 的安全核心:认证(Authentication)和授权(Authorization)。这两个词常被混用,但在框架里是两个明确分开的阶段。理解它们才能做好登录、JWT、权限控制。
一、认证 vs 授权
1
2
3
4
5
6
7
8
9
10
11
12
13
| 认证(Authentication)= 你是谁?
验证用户身份(用户名密码、Token、证书)
填充 HttpContext.User
"张三登录成功"
授权(Authorization)= 你能干什么?
基于已认证的身份,判断能否访问资源
读取 HttpContext.User 做决策
"张三是管理员,可以删用户"
执行顺序(中间件顺序):
UseAuthentication → UseAuthorization
先认证(你是谁)→ 再授权(能干嘛)
|
二、Claims 模型(身份的核心)
ASP.NET Core 用 Claims 模型表示身份,理解它很关键。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Claim = 一条身份信息(键值对)
new Claim(ClaimTypes.Name, "张三")
new Claim(ClaimTypes.Email, "zhang@test.com")
new Claim(ClaimTypes.Role, "Admin")
// ClaimsIdentity = 一组 Claim + 认证类型
var identity = new ClaimsIdentity(claims, "MyAuthScheme");
// ClaimsPrincipal = 一个用户(可含多个 Identity)
var principal = new ClaimsPrincipal(identity);
// 赋值给 HttpContext(认证完成)
HttpContext.User = principal;
|
1
2
3
4
5
6
| 层级关系:
ClaimsPrincipal(用户)
└── ClaimsIdentity(一种身份,如 JWT 身份)
└── Claim(一条信息,如姓名、角色)
用户的 Name、Role 都是从 Claim 读出来的
|
三、JWT 认证(最常用)
JSON Web Token,无状态、跨域、适合前后端分离和微服务。
3.1 配置
1
2
3
4
5
6
7
8
9
| // appsettings.json
{
"Jwt": {
"Issuer": "jiwei.space",
"Audience": "jiwei.space",
"SigningKey": "this-is-a-very-long-secret-key-at-least-32-chars",
"ExpiresMinutes": 120
}
}
|
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
| // Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var jwt = builder.Configuration.GetSection("Jwt").Get<JwtOptions>()!;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwt.Issuer,
ValidateAudience = true,
ValidAudience = jwt.Audience,
ValidateLifetime = true, // 验证过期
ValidateIssuerSigningKey = true, // 验证签名
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwt.SigningKey)),
ClockSkew = TimeSpan.Zero // 不允许时间偏差
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication(); // 先认证
app.UseAuthorization(); // 后授权
|
3.2 颁发 Token(登录)
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
| public class AuthService
{
private readonly JwtOptions _jwt;
public AuthService(IOptions<JwtOptions> opt) => _jwt = opt.Value;
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.Username),
new(ClaimTypes.Role, user.Role)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.SigningKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _jwt.Issuer,
audience: _jwt.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwt.ExpiresMinutes),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
// 登录接口
[HttpPost("login")]
public ActionResult Login(LoginDto dto)
{
var user = _userService.Verify(dto.Username, dto.Password);
if (user == null) return Unauthorized();
var token = _auth.CreateToken(user);
return Ok(new { token });
}
|
3.3 客户端使用
1
2
3
4
5
| 客户端登录拿到 token → 存 localStorage/cookie → 后续请求带上:
Authorization: Bearer eyJhbGciOi...
框架自动验证 token,验证通过填充 HttpContext.User
|
3.4 JWT 结构
1
2
3
4
5
6
7
8
9
10
| header.payload.signature(三段 Base64URL,用 . 分隔)
header = 算法(HS256)和类型(JWT)
payload = claims(用户信息、过期时间)
signature = 用密钥对 header.payload 签名(防篡改)
特点:
✓ 无状态(服务端不存 session,靠签名验证)
✓ 可跨服务(微服务共享密钥即可验证)
✗ 无法主动失效(签发后到过期前一直有效,靠 Refresh Token / 黑名单缓解)
|
四、Cookie 认证(传统 Web)
适合 MVC/Razor Pages 这类服务端渲染、浏览器访问的场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login"; // 未登录跳转
options.LogoutPath = "/logout";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.SlidingExpiration = true; // 滑动过期
});
// 登录(创建 Cookie)
public async Task Login(string username, string password)
{
var user = _userService.Verify(username, password);
if (user == null) return;
var claims = new[] { new Claim(ClaimTypes.Name, user.Username) };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity));
}
// 注销
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
1
2
3
4
5
6
| JWT vs Cookie:
JWT — 无状态、API/前后端分离、移动端、微服务
Cookie — 有状态、浏览器、MVC、SSO
前后端分离 → JWT
传统 Web → Cookie
|
五、授权
认证解决"你是谁",授权解决"能干什么"。
5.1 基础授权
1
2
3
4
5
6
7
8
9
10
| [Authorize] // 必须登录
public class UsersController { ... }
[Authorize]
[HttpGet("profile")]
public UserProfile GetProfile() { ... }
[AllowAnonymous] // 允许匿名(覆盖 Authorize)
[HttpPost("register")]
public User Register() { ... }
|
5.2 角色授权
1
2
3
4
5
6
| [Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public void Delete(int id) { ... } // 只有 Admin 角色能访问
[Authorize(Roles = "Admin,Manager")] // Admin 或 Manager
public void Manage() { ... }
|
1
2
3
| 角色怎么来:登录时把角色写进 Claim
new Claim(ClaimTypes.Role, "Admin")
授权时框架从 User.Claims 读角色比对
|
5.3 策略授权(推荐,最强大)
基于策略,比角色灵活——可以组合多个条件、自定义逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 1. 定义策略(包含要求 Requirement)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast18", policy =>
policy.RequireClaim("Age").RequireAssertion(ctx =>
int.Parse(ctx.User.FindFirst("Age")!.Value) >= 18));
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
// 组合策略
options.AddPolicy("SeniorAdmin", policy =>
policy.RequireRole("Admin").RequireClaim("Department", "IT"));
});
// 2. 使用
[Authorize(Policy = "AtLeast18")]
[HttpGet("adult-content")]
public Content GetAdult() { ... }
|
5.4 自定义授权要求
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
| // 1. 定义要求
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int min) => MinimumAge = min;
}
// 2. 定义处理器(业务逻辑)
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var ageClaim = context.User.FindFirst("Age");
if (ageClaim != null && int.Parse(ageClaim.Value) >= requirement.MinimumAge)
{
context.Succeed(requirement); // 满足
}
return Task.CompletedTask;
}
}
// 3. 注册
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy => policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// 4. 使用
[Authorize(Policy = "Over18")]
public IActionResult Drink() { ... }
|
5.5 基于资源的授权
授权决策依赖具体资源(如"只能编辑自己的文章"):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class ArticleOwnerHandler : AuthorizationHandler<ArticleOwnerRequirement, Article>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, ArticleOwnerRequirement req, Article article)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (article.AuthorId == userId)
context.Succeed(req);
return Task.CompletedTask;
}
}
// 使用(在 Action 里)
[HttpGet("{id}/edit")]
public async Task<IActionResult> Edit(int id)
{
var article = _service.Get(id);
var success = await _authorization.AuthorizeAsync(User, article, "OwnerPolicy");
if (!success.Succeeded) return Forbid();
return Ok(article);
}
|
六、安全最佳实践
1
2
3
4
5
6
7
8
9
| ✓ 密码用 BCrypt/PBKDF2/Argon2 哈希,绝不存明文
✓ JWT 密钥足够长(≥256bit),放配置/密钥库,不进代码
✓ HTTPS 强制(UseHttpsRedirection + HSTS)
✓ Cookie 设 HttpOnly、Secure、SameSite
✓ 防 CSRF(Cookie 场景用 AntiForgeryToken)
✓ 防 XSS(输出转义、CSP 头)
✓ 限流(防暴力破解登录)
✓ 敏感操作要二次验证
✓ 日志记录认证/授权事件
|
七、小结
- 认证 vs 授权:你是谁 / 能干什么,中间件顺序 UseAuthentication → UseAuthorization
- Claims 模型:Principal → Identity → Claim,身份的核心表示
- JWT:无状态、API 首选;Cookie:有状态、传统 Web
- 授权:
[Authorize] → 角色授权 → 策略授权(最强大)→ 基于资源授权
下一篇讲 API 设计:RESTful 规范、Swagger 文档、统一错误处理、版本控制。