ASP.NET Core 学习笔记(三):认证与授权

写在前面

本文讲 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 文档、统一错误处理、版本控制。