同一个后端接口,往往要同时服务好几种客户端:浏览器后台、微信小程序、App、第三方系统调用……浏览器喜欢 Cookie,自动携带还防 XSS;小程序没有 Cookie 环境,只能拿 Token 塞 Header。
两边都要支持,怎么办?只选一个,另一边就废了。
所以正确的答案是:两个都要。登录时同时返回 JSON 给前端 + 种 HttpOnly Cookie,请求时先读 Header,没有再从 Cookie 回退。一套认证逻辑同时兼容所有客户端,前端代码量不增反减。
这篇直接带你过一遍 .NET 6+ 里"Storage + Cookie 综合方案"的完整落地代码。
两种方案的根本差异
综合方案的核心思路:
同时支持两种方式——带 Authorization 头的请求走 Header,不带头的请求从 Cookie 回退读取。浏览器登录后自动走 Cookie,小程序/第三方调用时手动塞 Header,一套 [Authorize] 覆盖所有客户端。
怎么引入
需要安装的包
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Program.cs 完整配置
using Microsoft.AspNetCore.Authentication.JwtBearer;using Microsoft.AspNetCore.Http;using Microsoft.IdentityModel.Tokens;var builder = WebApplication.CreateBuilder(args);// ============ 1. CORS 配置 ============builder.Services.AddCors(options => options.AddPolicy("AllowFrontend", policy =>// ============ 2. 认证配置(仅 JWT Bearer,通过 OnMessageReceived 回退读 Cookie) ============builder.Services.AddAuthentication(options => options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; var jwtSettings = builder.Configuration.GetSection("JwtSettings"); options.TokenValidationParameters = new TokenValidationParameters ValidIssuer = jwtSettings["Issuer"], ValidAudience = jwtSettings["Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSettings["SecretKey"])), ClockSkew = TimeSpan.Zero options.Events = new JwtBearerEvents OnMessageReceived = context => string token = string.Empty; var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(authHeader)) if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) authHeader = "Bearer " + authHeader; token = authHeader.Substring("Bearer ".Length).Trim(); // 2. Header 没有,从 Cookie 回退 if (string.IsNullOrWhiteSpace(token)) var path = context.HttpContext.Request.Path.Value; if (path != null && path.EndsWith("/refresh", StringComparison.OrdinalIgnoreCase)) context.Request.Cookies.TryGetValue("refresh_token", out var refreshToken); context.Request.Cookies.TryGetValue("access_token", out var accessToken); return Task.CompletedTask;builder.Services.AddAuthorization();builder.Services.AddControllers();var app = builder.Build();app.UseHttpsRedirection();app.UseCors("AllowFrontend");
核心逻辑:
- OnMessageReceived 先读 Authorization 头,没有再读 Cookie。
- /refresh 请求从 refresh_token Cookie 取,普通请求从 access_token Cookie 取。
- 只注册 JWT Bearer 方案,不注册 AddCookie,避免方案混淆。
快速上手
示例一:登录接口
using Microsoft.AspNetCore.Mvc;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;[Route("api/[controller]")]public class AuthController : ControllerBase private readonly IWebHostEnvironment _env; public AuthController(IWebHostEnvironment env) public IActionResult Login([FromBody] LoginRequest request) if (request.Username != "admin" || request.Password != "123456") return Unauthorized(new { success = false, message = "用户名或密码错误" }); var user = new { Id = 1, Name = "Admin", Role = "Admin" }; var accessToken = GenerateAccessToken(user, TimeSpan.FromMinutes(15)); var refreshToken = GenerateRefreshToken(user, TimeSpan.FromDays(7)); bool isDevelopment = _env.IsDevelopment(); Response.Cookies.Append("access_token", accessToken, new CookieOptions SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddMinutes(15), Response.Cookies.Append("refresh_token", refreshToken, new CookieOptions SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(7), Path = "/api/auth/refresh" refreshToken = refreshToken, private string GenerateAccessToken(object user, TimeSpan expiresIn) new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Name), new Claim(ClaimTypes.Role, user.Role) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key")); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( audience: "your-audience", expires: DateTime.UtcNow.Add(expiresIn), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); private string GenerateRefreshToken(object user, TimeSpan expiresIn) new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim("token_type", "refresh") var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key")); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( audience: "your-audience", expires: DateTime.UtcNow.Add(expiresIn), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token);public class LoginRequest public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
示例二:刷新 Token 接口
public IActionResult Refresh() var refreshToken = GetRefreshTokenFromRequest(Request); if (string.IsNullOrEmpty(refreshToken)) return Unauthorized(new { message = "No refresh token" }); var principal = ValidateRefreshToken(refreshToken); return Unauthorized(new { message = "Invalid refresh token" }); var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId)) return Unauthorized(new { message = "Invalid refresh token payload" }); var user = new { Id = int.Parse(userId), Name = "Admin", Role = "Admin" }; var newAccessToken = GenerateAccessToken(user, TimeSpan.FromMinutes(15)); var newRefreshToken = GenerateRefreshToken(user, TimeSpan.FromDays(7)); bool isDevelopment = _env.IsDevelopment(); Response.Cookies.Append("access_token", newAccessToken, new CookieOptions SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddMinutes(15), Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(7), Path = "/api/auth/refresh" refreshToken = newRefreshToken,private string GetRefreshTokenFromRequest(HttpRequest request) var authHeader = request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")) return authHeader["Bearer ".Length..]; request.Cookies.TryGetValue("refresh_token", out var token);private ClaimsPrincipal ValidateRefreshToken(string token) var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes("your-secret-key"); var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters ValidateIssuerSigningKey = true, ValidIssuer = "your-issuer", ValidAudience = "your-audience", IssuerSigningKey = new SymmetricSecurityKey(key)
示例三:退出登录
public IActionResult Logout() Response.Cookies.Delete("access_token", new CookieOptions { Path = "/" }); Response.Cookies.Delete("refresh_token", new CookieOptions { Path = "/api/auth/refresh" }); return Ok(new { success = true, message = "已退出登录" });
前端怎么用
浏览器端
const res = await axios.post('/api/auth/login', {axios.defaults.withCredentials = true;const data = await axios.get('/api/data');
小程序端
// 1. 登录——从响应里取 token 存缓存const res = await request({ url: '/api/auth/login', method: 'POST', data: {...} });wx.setStorageSync('access_token', res.data.token);wx.setStorageSync('refresh_token', res.data.refreshToken);// 2. 后续请求——手动塞 Authorization 头const token = wx.getStorageSync('access_token'); url: 'https://api.yourdomain.com/api/data', header: { 'Authorization': `Bearer ${token}` }
小程序刷新 token 注意:小程序没有 Cookie,刷新时需手动从缓存取 refresh_token 塞 Authorization 头,不能直接调 axios.post('/refresh')。
常见报错与修复
| | |
|---|
| 跨域未配置 credentials 或 Path 不匹配 | 前端 withCredentials: true,后端 .AllowCredentials() |
| | Path 改为 /api/auth/refresh |
| WithOrigins | |
| Secure = true | 用 _env.IsDevelopment() 动态切换 |
| | 从缓存取 refresh_token 塞 Header |
选型建议
总结收尾
一个后端接口可能要服务多个客户端——浏览器喜欢 Cookie,小程序没有 Cookie 环境。只选一个,另一边就废了。
答案是:两个都要。
综合方案的代码核心就三点:
1. OnMessageReceived 里先读 Header,没有再读 Cookie。
2. 登录时同时返回 JSON + 种 HttpOnly Cookie,Cookie 的 Path 必须与实际路由一致。
3. 退出时直接删除 Cookie,不依赖 SignOutAsync。
三条落地建议: 1. 先搞清楚客户端类型:只有浏览器?还是有小程序/App?
2. 用文中的 AddJwtBearer + OnMessageReceived 模板,改一下密钥。
3. 跨域场景前端 withCredentials: true,后端 .AllowCredentials()。