1. 前言
框架为我们提供了很多默认的实现,在做一些中小项目,在不涉及OIDC,不构建身份认证中心的场景下,通常选择使用Cookie和JWT的方式。同时框架内置了其他的一些实现,例如基于oAuth2.0的实现,包括谷歌、Facebook、GitHub等用于支持第三方登录。
在我们日常开发场景下,可能会接触使用一些,飞书、微信等第三方登录,但是原生框架中并没有直接提供实现。因为他们也遵循 oAuth2.0协议标准的,我们可以自己封装集成进框架中,注意是实现授权码流程,还有其他几种流程模式,可能不支持,如果要区分出他们有什么不同的话可能需要搞清楚一些概念。
1. 什么是OIDC?
2.什么是oAuth2.0?
这里就不多做介绍,比较偏理论,理解串起来很头疼,但是个人觉得还是得静下心来找一些书籍或者项目自己研究下,不要草草的复制粘贴进AI工具,回答的快忘记的也快,AI作为辅助还可以,成体系还是得自己梳理。
2. 使用Cookie的认证方式
在介绍之前,我们必须先搞清楚一个概念,有状态和无状态,一句话就是指的是服务端要不要保存会话,下面是无状态的请求流程和特点。

特点:
- 服务端没有会话记录,重启应用不影响已发出的 Cookie(密钥还在就行)
- 退出登录 = 只删浏览器 Cookie,服务端没有会话
- 别人复制 Cookie,在你退出登录前可能还有效
- 多实例部署比较简单,每台机器有一样的密钥就行
- 因为Claims 都在里面,所以Cookie 很长
下面是有状态流程和特点:

特点:
- 服务端有会话记录,能强制下线、踢人、查在线。
- 退出登录 = 删服务端会话 + 清 Cookie,旧 Cookie 立刻失效。
- 多实例部署要 Redis 等共享存储,否则会话查不到。
- Cookie 通常比无状态略短,但还是长密文,不是理解的那种guid那种。
- 应用重启如果在内存的话会话全丢,用户要重新登录。
可以对比表格看下:
| 对比维度 | 无状态(默认 AddCookie) | 有状态(SessionStore / ITicketStore) |
|---|---|---|
| 服务端是否存会话 | 不存 | 存(内存 / Redis / DB) |
| Cookie 里主要是什么 | 加密后的完整 Ticket(含 Claims) | 加密后的会话引用(SessionId) |
| 每次请求怎么认人 | 解密 Cookie → 直接得到 User | 解密 Cookie → 拿 SessionId → 查服务端 → 得到 User |
.NET Core 的 Cookie 认证方案在默认配置下是无状态的,服务端不会存储认证票据,服务端只需要:
- 从 HTTP 请求中读取 Cookie
- 使用
TicketDataFormat解密 - 验证票据是否过期
当然也可以使用有状态,主要用于处理票据很大,超过浏览器 Cookie 大小限制或需要做即时撤销的场景,例如服务端踢人下线,又或者是需要在多个服务实例间共享会话状态。
我们先介绍在NETCORE中Cookie认证使用无状态模式怎么做。
2.1 无状态Cookie
集成思路:

第一步:注册身份认证核心服务
需要先在 Program 注册身份认证的核心服务以及常规配置,使中间件可以调用对应服务。
var authenticationBuilder = builder.Services.AddAuthentication(options =>
{
// 设置默认的身份验证方案(用于验证用户身份)
// 当控制器未显式指定 AuthenticationSchemes 时,系统自动使用 Cookie 方案
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// 设置默认的质询方案(用于触发登录流程)
// 当未认证用户访问受保护资源时,系统自动重定向到 Cookie 登录流程
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// 要求用户必须通过 SignInManager.SignInAsync() 完成登录
// 防止仅通过 ClaimsPrincipal 构造的"伪登录"绕过安全检查
options.RequireAuthenticatedSignIn = true;
});
第二步:将Cookie认证方案和它的配置注入容器
// 添加Cookie 身份验证方案
authenticationBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
// 设置 Cookie 的名称(浏览器中显示为 CodeSource.Auth)
// 不同应用必须使用唯一名称避免跨站冲突
options.Cookie.Name = "CodeSource.Auth";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(8); // 过期时间
options.SlidingExpiration = true; // 滑动过期
// 针对没认证但是访问资源,将原本的重定向改为直接返回 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;
};
});
第三步:注册中间件
通过 app.UseXXX() 方法将认证与授权中间件按顺序加入请求处理管道,这里需要注意顺序,先认证,再授权,所以中间件必须也要这样定义。至于为什么,可以自行去补充下基础知识。
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
}
app.UseHttpsRedirection();
app.UseSwaggerUI();
// 添加认证中间件
app.UseAuthentication();
// 添加授权中间件
app.UseAuthorization();
app.MapControllers();
app.Run();
第四步:在控制器中使用
注意标记 Authorize 特性,然后认证 Scheme 选择 Cookie 的方式。括号中的如果只有一种认证方式可以不写,因为在前面配置时设置了默认方案。
[ApiController]
[Route("[controller]")]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public class UserController : ControllerBase
{
private static readonly User[] Users =
[
new User { Id = 1, Name = "张三", Email = "zhangsan@example.com" },
new User { Id = 2, Name = "李四", Email = "lisi@example.com" },
new User { Id = 3, Name = "王五", Email = "wangwu@example.com" }
];
[HttpGet(Name = "GetUsers")]
public IEnumerable Get()
{
return Users;
}
}
2.2 有状态Cookie
相比上面无状态模式,有状态模式需要额外加一些代码用来存储读写和过期清理。如果是单服务实例,可以直接存入本地内存,但需注意服务重启的话会导致票据丢失,用户直接退出登录了。如果服务多实例负载均衡的情况下需要引入分布式存储来共享,把票据存入Redis或者Memcached中。如果你坚持本地内存可能会破坏负载均衡策略。
集成思路:

第一步:将认证方案注入容器
将认证方案,例如 Cookie 和它的配置注入容器,跟无状态一模一样的套路,就不贴代码了,参照无状态第1、2步。
第二步:实现 ITicketStore 接口
如果需要使用Cookie的有状态模式,框架的 CookieAuthenticationOptions 选项提供了 SessionStore 配置来进行实现,他是 ITicketStore 类型,这个接口是框架提供给用户扩展的,主要用于认证票据存在哪,我们需要实现它。
public class MemoryTicketStore(private readonly IMemoryCache _cache) : ITicketStore
{
private const string KeyPrefix = "auth-session:";
// 存
public Task StoreAsync(AuthenticationTicket ticket)
{
var key = $"{KeyPrefix}{Guid.NewGuid():N}";
RenewAsync(key, ticket);
return Task.FromResult(key);
}
// 更新
public Task RenewAsync(string key, AuthenticationTicket ticket)
{
var expires = ticket.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.AddHours(8);
_cache.Set(key, ticket, new MemoryCacheEntryOptions
{
AbsoluteExpiration = expires
});
return Task.CompletedTask;
}
// 查询
public Task RetrieveAsync(string key)
=> Task.FromResult(_cache.Get(key));
// 删除
public Task RemoveAsync(string key)
{
_cache.Remove(key);
return Task.CompletedTask;
}
}
你同样可以自己实现Redis存储的逻辑,只需替换 MemoryTicketStore 为 RedisTicketStore,然后修改 ITicketStore 的注册,根本不需要改动身份验证配置。为了方便我使用的内存缓存,接下来需要将实现注入容器,再将它配置起来。
第三步:注入并配置
// 注入组件
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<ITicketStore, MemoryTicketStore>();
// 配置实例延迟执行,防止依赖的服务没注册
builder.Services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<ITicketStore>((options, store) => options.SessionStore = store);
第四步:在控制器中使用
在控制器中使用和无状态模式一样使用。
2.3 使用 CookieAuthentication 登录实现
用 Cookie 认证的方式登录,不需要我们实现生成 Cookie,框架完全控制会话生命周期,SignInAsync() 触发标准化的 Cookie 生成流程,登录代码只需要构建 principal 就行。
- 构建
ClaimsPrincipal(含用户标识和权限声明) - 调用
SignInAsync()传递该主体
public async Task Login([FromBody] LoginRequest request)
{
if (request.Username != "admin" || request.Password != "123456")
{
return Unauthorized(new { message = "用户名或密码错误" });
}
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Role, "User")
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(8)
});
// 模式一:return Ok(new { message = "登录成功" });
return Ok(new { message = "登录成功,会话已保存在服务端" }); // 模式二(当前)
}
3. 使用JWT的认证方式
3.1 集成JWT
接下来我们继续看看JWT认证的方式如何在NETCORE中集成和实现,其实jwt也是无状态的,通常服务器一旦颁发,在token过期之前后期是不可控的。
集成思路:

第一步:注册JWT服务
和集成Cookie认证一样,都需要先注册,然后启用认证中间件,而这里注册的是JWT相关的服务。
var authenticationBuilder = builder.Services.AddAuthentication(options =>
{
// 将默认方案设置为JWT
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
});
authenticationBuilder.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 验证签发者,令牌中的 "iss" 字段必须与 ValidIssuer 完全匹配
ValidateAudience = true, // 是否验证受众,保证令牌是发给当前服务的
ValidateLifetime = true, // 是否验证令牌有效期
ValidateIssuerSigningKey = true, // 是否验证令牌签名
ValidIssuer = jwtIssuer, // 合法签发者标识 "https://鉴权服务.com"
ValidAudience = jwtAudience, // 受众标识
// 令牌签名验证密钥,对称加密
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("JWTjiamimiyao!"))
};
// JWT 认证流程中的事件钩子,未认证时触发
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
// 移除 WW-Authenticate 避免暴露敏感信息
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
return context.Response.WriteAsync(JsonSerializer.Serialize(new
{
error = "未认证",
message = "Token无效或者过期."
}));
}
};
});
第二步:在接口中使用
直接接口中使用 [Authorize(AuthenticationSchemes = "Bearer")] 就代表这一组接口都会启用jwt认证的方式来认证,框架就可以完成验签与claims的解析。
[ApiController]
[Route("[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ProductController : ControllerBase
{
private static readonly Product[] Products =
[
new Product { Id = 1, Name = "机械键盘", Price = 599.00m }
];
[HttpGet("{id:int}", Name = "GetProductById")]
public ActionResult GetById(int id)
{
var product = Products.FirstOrDefault(p => p.Id == id);
if (product is null)
{
return NotFound();
}
return product;
}
}
那么集成进来之后,当有接口请求时,就会按照如下流程来执行:

因为定义了未授权事件,一旦触发就会收到友好的提示:

3.2 使用 JwtAuthentication 登录实现
在使用jwt认证模式,登录实现机制和cookie有一个最大区别就是颁发凭证的逻辑需要自己完成,并且在登录时不需要手动构造 ClaimsIdentity 或 ClaimsPrincipal 对象。
第一步:实现生成Token的服务
需要先实现生成token的服务,然后注入到DI容器。
public class JwtTokenService(IConfiguration configuration)
{
public string GenerateToken(string username) =>
new JwtSecurityTokenHandler().WriteToken(
new JwtSecurityToken(
issuer: configuration["Jwt:Issuer"]!,
audience: configuration["Jwt:Audience"]!,
claims: [
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, "User")
],
expires: DateTime.UtcNow.AddHours(
configuration.GetValue("Jwt:ExpireHours", 8)
),
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)
),
SecurityAlgorithms.HmacSha256
)
)
);
}
第二步:在登录代码中签发Token
在登录的代码中用户账密验证成功后签发token。
public IActionResult Login([FromBody] LoginRequest request)
{
if (request.Username != "admin" || request.Password != "123456")
{
return Unauthorized(new { message = "用户名或密码错误" });
}
var token = _jwtTokenService.GenerateToken(request.Username);
return Ok(new
{
message = "登录成功,要在请求头携带 Authorization: Bearer {token}",
token,
tokenType = "Bearer",
expiresInHours = 8
});
}
4. 总结
在 .NET Core 身份验证体系中,可以发现使用 CookieAuthentication 和 JwtAuthentication,我们只需要配置就行了,有些概念我们完全不用担心,例如登录成功后怎么设置 Cookie,然后请求到达如何验证Cookie或者Token是否合法,这些都是框架在背后帮我们完成了。
Cookie适用于有页面的例如MVC、RazorPage,Token就比较适合纯Api,以及三方集成登录。有小伙伴可能会问,那我同时在系统加入Token和JWT能行吗?会不会有什么问题,其实不会有问题的,甚至可以更多,反而能灵活应对不同客户端的需求。如何做呢?
.NET Core 的认证模块,允许我们使用多种 Scheme,这些 Scheme 可以是框架提供的,也可以是我们自己扩展的。例如我在接口中同时引入 JWT 和 Cookie,那么就代表这两种方式任一一种通过接口就能通过,注意不是并且的关系。
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public class ProductController : ControllerBase
{
}
也可以不同接口使用不同的认证策略,同时除了可以标记在控制器,也可以允许放在具体的 Action 上。
[Authorize(AuthenticationSchemes = "CookieScheme")] // 仅允许 Cookie
public IActionResult MvcAction() { ... }
[Authorize(AuthenticationSchemes = "JwtScheme")] // 仅允许 JWT
public IActionResult ApiAction() { ... }
甚至你可以配置所有的都必须经过认证,你可能会问,我有的接口不想认证怎么办,那就在接口上显式标记 [AllowAnonymous]。
builder.Services.AddAuthorization(options =>
{
// 设置默认策略必须要求已认证用户
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
再或者有少数情况使用过滤器也可以实现,例如你要实现0点到6点有些接口不允许访问就可以使用过滤器来实现。
public class TimeRestrictionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var now = DateTime.Now;
if (now.Hour >= 0 && now.Hour < 6)
{
context.Result = new ForbidResult("禁止在 0:00-6:00 访问");
}
}
}
有时候标准的凭证传递方式不能实现,内置的 JWT 默认从 Authorization: Bearer 头读取 Token,Cookie 从请求头中读取。如果你的程序客户端是 IoT 设备、老旧系统,必须通过请求体方式传递凭证咋办,那就用不了了,这个时候就可以自定义 Scheme。
第一步:实现选项类
先实现一个继承自 AuthenticationSchemeOptions 的选项类。
public class BodyAuthenticationOptions : AuthenticationSchemeOptions
{
public string ApiKeyFieldName { get; set; } = "apiKey";
public string UserIdFieldName { get; set; } = "userId";
public string ExpectedApiKey { get; set; } = "default-key";
}
第二步:自定义认证处理Handler
public class BodyAuthenticationHandler : AuthenticationHandler<BodyAuthenticationOptions>
{
public BodyAuthenticationHandler(
IOptionsMonitor<BodyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 只处理 POST 请求
if (!HttpContext.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase))
{
return AuthenticateResult.NoResult();
}
// 确保请求体可重复读取
HttpContext.Request.EnableBuffering();
// 读取并解析 JSON Body
var body = await JsonDocument.ParseAsync(HttpContext.Request.Body);
if (!body.RootElement.TryGetProperty(Options.ApiKeyFieldName, out var apiKeyProp))
{
return AuthenticateResult.Fail($"Missing field: {Options.ApiKeyFieldName}");
}
var providedKey = apiKeyProp.GetString();
if (string.IsNullOrEmpty(providedKey) || providedKey != Options.ExpectedApiKey)
{
return AuthenticateResult.Fail("Invalid API Key");
}
string userId = null;
if (body.RootElement.TryGetProperty(Options.UserIdFieldName, out var userIdProp))
{
userId = userIdProp.GetString();
}
// 构造 Claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userId ?? "unknown")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
第三步:注册到容器
然后在启动时注册到容器,应用的话就在接口上标记对应的scheme名称就可以。
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "BodyAuth";
})
.AddScheme<BodyAuthenticationOptions, BodyAuthenticationHandler>("BodyAuth", opt =>
{
opt.ExpectedApiKey = "my-secret-key";
opt.ApiKeyFieldName = "apiKey";
opt.UserIdFieldName = "userId";
});
在我很长一段开发历程中,几乎没有接触过这些东西,相信大部分人和我一样,有时候项目小没必要关心这些,项目大也可能轮不着关心,甚至可能连实际搭一套登录鉴权体系的机会都没有,所以对这部分有点陌生。今天主要分享本地登录下的认证,就是自建系统登录加鉴权,不涉及第三方登录,后续会继续这一部分的深入。
浙公网安备 33010602011771号