
刚学 Minimal API 时,接口都很简单:
app.MapGet("/hello", () => "hello");你会觉得:
但是一到认证,就开始糊涂:
AddAuthentication()?AddAuthorization()?UseAuthentication()?.RequireAuthorization() 是干嘛的?所以这篇文章我会严格按这个顺序来讲:
也就是说:
先让你看懂最小闭环,再逐步升级。
你先不要急着看 Cookie 和 JWT。 先记住:Minimal API 的认证能不能跑起来,至少依赖 4 个东西。
builder.Services.AddAuthentication(...);它的意思是:
我要启用 ASP.NET Core 的认证系统。
builder.Services.AddAuthorization();它的意思是:
我要启用授权系统,这样接口才能要求“必须登录后才能访问”。
app.UseAuthentication();它的意思是:
每次请求进来时,框架尝试识别当前用户是谁。
app.UseAuthorization();它的意思是:
如果某个接口要求必须认证,那就在执行接口前检查用户是否已登录。
.RequireAuthorization()它的意思是:
这个接口不能匿名访问,必须先通过认证。
在 Minimal API 中:
AddAuthentication:注册认证系统
AddAuthorization:注册授权系统
UseAuthentication:请求来了先识别用户
UseAuthorization:再判断用户能不能访问
RequireAuthorization:声明这个接口必须登录你后面学 Cookie、JWT,本质上都离不开这 5 个点。
这一步我们的目标只有一个:
让你亲眼看到:一个接口可以被保护起来。
为了简单,我们先不用 Cookie,也不用 JWT。 我们先做一个“假认证”:
X-User这样可以让你先理解 Minimal API 认证框架本身怎么工作的
新建一个 Program.cs,直接复制下面代码:
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// 1. 注册认证:使用我们自定义的 Simple 方案
builder.Services
.AddAuthentication("Simple")
.AddScheme<AuthenticationSchemeOptions, SimpleAuthHandler>("Simple", options => { });
// 2. 注册授权
builder.Services.AddAuthorization();
var app = builder.Build();
// 3. 启用认证和授权中间件
app.UseAuthentication();
app.UseAuthorization();
// 公开接口,不需要登录
app.MapGet("/", () => "这是公开接口,任何人都能访问");
// 受保护接口,需要登录
app.MapGet("/me", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Message = "你已登录",
Name = user.Identity?.Name,
IsAuthenticated = user.Identity?.IsAuthenticated
});
}).RequireAuthorization();
app.Run();
// 自定义认证处理器
public class SimpleAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public SimpleAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 从请求头中读取 X-User
var userName = Request.Headers["X-User"].ToString();
// 如果没有这个请求头,就表示没有认证信息
if (string.IsNullOrWhiteSpace(userName))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
// 构造用户声明(Claims)
var claims = new[]
{
new Claim(ClaimTypes.Name, userName)
};
// 构造身份对象
var identity = new ClaimsIdentity(claims, Scheme.Name);
// 构造当前用户
var principal = new ClaimsPrincipal(identity);
// 构造认证票据
var ticket = new AuthenticationTicket(principal, Scheme.Name);
// 返回认证成功
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}请求:
GET /响应:
这是公开接口,任何人都能访问请求:
GET /me响应:
401 Unauthorized请求:
GET /me
X-User: alice响应:
{
"message": "你已登录",
"name": "alice",
"isAuthenticated": true
}这一段非常重要。 你如果把这一节看懂了,后面 Cookie 和 JWT 会轻松很多。
AddAuthentication("Simple") builder.Services.AddAuthentication("Simple")意思是:
注册认证系统,并告诉框架默认使用名为
Simple的认证方案。
你可以把“认证方案”理解为:
框架识别用户的一套规则
比如:
X-User 里识别用户.AddScheme<AuthenticationSchemeOptions, SimpleAuthHandler>() .AddScheme<AuthenticationSchemeOptions, SimpleAuthHandler>("Simple", options => { });这句的意思是:
注册一个名为
Simple的认证方案,它的具体处理逻辑由SimpleAuthHandler来完成。
你可以理解成:
AddAuthentication("Simple"):告诉框架默认用哪个方案AddScheme(..., "Simple", ...):把这个方案真正定义出来AddAuthorization() builder.Services.AddAuthorization();这句的作用是:
开启授权系统,否则
.RequireAuthorization()不会按预期工作。
很多小白的误区是:
以为“认证”和“授权”是一个东西
其实不是。
所以你想让接口“必须登录”,不仅要有认证,还要有授权。
app.UseAuthentication() app.UseAuthentication();作用:
在每个请求执行时,先调用认证系统,尝试识别当前用户。
也就是说,请求到了以后,框架会去执行你的 SimpleAuthHandler。
app.UseAuthorization() app.UseAuthorization();作用:
如果某个接口标记了必须认证,那就在执行接口之前检查当前用户是否通过了认证。
.RequireAuthorization() app.MapGet("/me", ...).RequireAuthorization();作用:
这个接口不能匿名访问,必须登录后才能访问。
注意,这一句不是“执行认证”,而是“声明要求”。
真正的认证动作,是 UseAuthentication() 在请求管道里做的。
你看到这段代码:
app.MapGet("/me", (ClaimsPrincipal user) =>
{
return user.Identity?.Name;
}).RequireAuthorization();可能会问:
为什么这里我直接写一个
ClaimsPrincipal user,框架就知道把当前用户传进来?
这是 Minimal API 的一个重要特性:
参数绑定
框架会自动识别你的委托参数类型,如果它发现参数是 ClaimsPrincipal,就会把当前请求的:
HttpContext.User注入进来。
所以:
(ClaimsPrincipal user)本质上就是:
(HttpContext context) => context.User的简化写法。
HttpContext比如:
app.MapGet("/me2", (HttpContext context) =>
{
var user = context.User;
return Results.Ok(new
{
Name = user.Identity?.Name,
IsAuthenticated = user.Identity?.IsAuthenticated
});
}).RequireAuthorization();这两种写法都可以。
对于小白来说,我建议一开始优先用:
(ClaimsPrincipal user)更直观。
现在我们已经知道 Minimal API 的认证骨架了。 接下来开始学习真实项目里最常见的第一种认证方式:
Cookie Authentication
它特别适合:
因为浏览器会自动带 Cookie。
刚刚我们的 Simple 方案只是“演示框架怎么工作”。
现在换成 Cookie 后,变化是:
X-User 了 using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
// 1. 注册 Cookie 认证
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// 设置 Cookie 名称
options.Cookie.Name = "demo.auth";
// 对 API 项目来说,未登录时不要跳转页面,直接返回 401
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
// 没权限时返回 403
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
});
// 2. 注册授权
builder.Services.AddAuthorization();
var app = builder.Build();
// 3. 启用认证和授权中间件
app.UseAuthentication();
app.UseAuthorization();
// 公开接口
app.MapGet("/", () => "公开接口");
// 登录接口
app.MapPost("/login", async (HttpContext context) =>
{
// 这里为了演示,假设登录成功
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "alice"),
new Claim(ClaimTypes.NameIdentifier, "1001")
};
var identity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
// 写入认证 Cookie
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal);
return Results.Ok("登录成功");
});
// 当前用户接口
app.MapGet("/me", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Name = user.Identity?.Name,
UserId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
IsAuthenticated = user.Identity?.IsAuthenticated
});
}).RequireAuthorization();
// 退出登录
app.MapPost("/logout", async (HttpContext context) =>
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.Ok("退出成功");
}).RequireAuthorization();
app.Run();/me因为还没登录,所以返回:
401 Unauthorized/login POST /login服务器会返回成功,并在响应头里写入认证 Cookie。
/me如果浏览器自动带上 Cookie,就能拿到:
{
"name": "alice",
"userId": "1001",
"isAuthenticated": true
}/logout POST /logout退出后再访问 /me,又会变成 401。
AddCookie(...) 是注册 Cookie 认证方案 builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(...)它的意思是:
默认认证方式是 Cookie,并使用 ASP.NET Core 内置的 Cookie 处理器。
SignInAsync(...) 是登录的关键 await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal);这句非常关键。
它不是简单写一个字符串 Cookie,而是:
ClaimsPrincipal 作为当前登录用户后续请求时,框架再把这个 Cookie 解出来,还原成用户对象。
SignOutAsync(...) 是退出登录的关键 await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);作用:
删除或失效当前认证 Cookie。
比如读取用户 ID:
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;比如读取用户名:
var userName = user.Identity?.Name;如果你的项目不是浏览器后台,而是:
那么更常用的是:
JWT Bearer 认证
登录成功后,服务器写 Cookie,浏览器自动带上。
登录成功后,服务器返回一个 Token,客户端自己保存,以后每次手动放到请求头里。
也就是说:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
var issuer = "demo-api";
var audience = "demo-client";
var secretKey = "this_is_a_demo_secret_key_1234567890";
// 1. 注册 JWT 认证
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(secretKey)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// 2. 注册授权
builder.Services.AddAuthorization();
var app = builder.Build();
// 3. 启用认证和授权
app.UseAuthentication();
app.UseAuthorization();
// 公开接口
app.MapGet("/", () => "公开接口");
// 登录接口:生成 JWT
app.MapPost("/login", () =>
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "alice"),
new Claim(ClaimTypes.NameIdentifier, "1001")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: credentials);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return Results.Ok(new
{
access_token = jwt
});
});
// 受保护接口
app.MapGet("/me", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Name = user.Identity?.Name,
UserId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
IsAuthenticated = user.Identity?.IsAuthenticated
});
}).RequireAuthorization();
app.Run(); POST /login返回:
{
"access_token": "eyJhbGciOi..."
}/me 时带上 Token请求头:
Authorization: Bearer eyJhbGciOi...返回:
{
"name": "alice",
"userId": "1001",
"isAuthenticated": true
}如果不带 Token,就返回 401。
这是很多小白很容易混的地方。
两者在 Minimal API 中的框架用法其实很像:
都需要:
builder.Services.AddAuthentication(...)
builder.Services.AddAuthorization()
app.UseAuthentication()
app.UseAuthorization()
app.MapGet(...).RequireAuthorization()也都可以这样拿用户:
(ClaimsPrincipal user)你要调用:
await context.SignInAsync(...)让服务器把认证状态写到 Cookie。
你要自己生成 Token:
new JwtSecurityToken(...)再把它返回给客户端。
Cookie:服务器发 Cookie,浏览器自动带
JWT:服务器发 Token,客户端手动带现在我们已经会了基本认证。 接下来进入比“是否登录”更进一步的一步:
不是所有登录用户都能访问某个接口
比如:
/admin这就是角色控制。
不管 Cookie 还是 JWT,思路都一样。 你只需要给用户身份加上角色 Claim:
new Claim(ClaimTypes.Role, "admin")例如 JWT 登录里:
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "alice"),
new Claim(ClaimTypes.NameIdentifier, "1001"),
new Claim(ClaimTypes.Role, "admin")
}; app.MapGet("/admin", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Message = "欢迎管理员",
Name = user.Identity?.Name
});
}).RequireAuthorization(policy => policy.RequireRole("admin"));如果当前用户没有 admin 角色,就会返回:
403 Forbidden注意:
当项目变大以后,你不能每个接口都写一遍:
.RequireAuthorization(policy => policy.RequireRole("admin"))这会很乱。
更好的方式是定义 Policy。
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
{
policy.RequireRole("admin");
});
}); app.MapGet("/admin2", () => "只有管理员能访问")
.RequireAuthorization("AdminOnly");这样代码更清晰。
这是 Minimal API 非常有代表性的用法:
用
MapGroup()对一组接口统一做认证控制
var userGroup = app.MapGroup("/user")
.RequireAuthorization();
userGroup.MapGet("/me", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Name = user.Identity?.Name
});
});
userGroup.MapGet("/settings", () => "用户设置");这样:
/user/me/user/settings都会要求登录。
var adminGroup = app.MapGroup("/admin")
.RequireAuthorization("AdminOnly");
adminGroup.MapGet("/dashboard", () => "管理员仪表盘");
adminGroup.MapGet("/users", () => "用户列表");这比每个接口单独写要整洁很多。
这一节我专门按“最容易错”的点来讲。
UseAuthentication()你就算写了:
builder.Services.AddAuthentication(...)如果没有:
app.UseAuthentication();用户身份也不会自动识别出来。
UseAuthentication() 和 UseAuthorization() 顺序写反必须这样:
app.UseAuthentication();
app.UseAuthorization();不能反。
因为必须先识别用户,再判断权限。
不是的。
你还要显式声明:
.RequireAuthorization()否则接口默认还是可以匿名访问。
记住:
没登录,或者凭证无效
已经登录,但没有权限
如果你用 Cookie 认证,但不改默认行为,未登录时可能返回 302 跳转,而不是 401。
所以 API 场景一般都要加:
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。