.NET Core 6无状态微服务:无Cookie的OAuth2/OpenIDConnect实现方案
1. 基础认证配置(不依赖持久化会话)
在Program.cs中配置OIDC时,指定一个仅满足框架要求的空SignInScheme,同时禁用默认会话处理,拦截授权码交换Token的流程:
builder.Services.AddAuthentication(options => { options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { options.ClientId = "你的客户端ID"; options.ClientSecret = "你的客户端密钥"; options.Authority = "https://你的身份提供商地址"; options.ResponseType = "code"; options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("offline_access"); // 如需刷新Token可添加 // 指定空的SignInScheme,避免默认Cookie会话创建 options.SignInScheme = "DummySignIn"; options.SaveTokens = false; options.Events = new OpenIdConnectEvents { OnAuthorizationCodeReceived = async context => { // 用授权码换取Token var tokenClient = context.HttpContext.RequestServices.GetRequiredService<ITokenClient>(); var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest { Address = $"{options.Authority}/connect/token", ClientId = options.ClientId, ClientSecret = options.ClientSecret, Code = context.ProtocolMessage.Code, RedirectUri = context.ProtocolMessage.RedirectUri }); // 将Token暂存到HttpContext,供回调页面使用 context.HttpContext.Items["AccessToken"] = tokenResponse.AccessToken; context.HttpContext.Items["IdToken"] = tokenResponse.IdToken; context.HttpContext.Items["RefreshToken"] = tokenResponse.RefreshToken; // 跳过默认的会话创建流程 context.HandleResponse(); }, OnRedirectToIdentityProviderForSignOut = context => { // 自定义登出跳转,引导前端清除Session Storage context.Response.Redirect("/signout-callback"); context.HandleResponse(); return Task.CompletedTask; } }; }) // 添加空的认证Scheme,仅满足框架接口要求 .AddScheme<DummyAuthenticationOptions, DummyAuthenticationHandler>("DummySignIn", options => { });
2. 实现空的认证Handler
创建一个无实际逻辑的认证Handler,仅实现IAuthenticationSignInHandler接口以满足框架要求:
public class DummyAuthenticationHandler : AuthenticationHandler<DummyAuthenticationOptions>, IAuthenticationSignInHandler { public DummyAuthenticationHandler(IOptionsMonitor<DummyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { return Task.FromResult(AuthenticateResult.Fail("空认证方案")); } public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { return Task.CompletedTask; } public Task SignOutAsync(AuthenticationProperties properties) { return Task.CompletedTask; } public Task ChallengeAsync(AuthenticationProperties properties) { return Task.CompletedTask; } } public class DummyAuthenticationOptions : AuthenticationSchemeOptions { }
3. 回调端点与Token存储逻辑
添加一个回调端点,返回内嵌JavaScript的HTML页面,将Token存储到浏览器Session Storage后跳转至前端页面:
app.MapGet("/oidc-callback", async context => { if (!context.Items.TryGetValue("AccessToken", out var accessTokenObj) || !context.Items.TryGetValue("IdToken", out var idTokenObj)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Token获取失败"); return; } var accessToken = accessTokenObj.ToString(); var idToken = idTokenObj.ToString(); var refreshToken = context.Items["RefreshToken"]?.ToString(); var html = $@" <!DOCTYPE html> <html> <head> <title>认证回调处理</title> <script> // 将Token存入Session Storage sessionStorage.setItem('access_token', '{accessToken}'); sessionStorage.setItem('id_token', '{idToken}'); {!string.IsNullOrEmpty(refreshToken) ? $"sessionStorage.setItem('refresh_token', '{refreshToken}');" : ""} // 跳转到前端首页 window.location.href = '/前端首页路径'; </script> </head> <body> 正在处理认证... </body> </html>"; context.Response.ContentType = "text/html"; await context.Response.WriteAsync(html); });
4. 前端请求与网关校验
前端通过请求拦截器自动携带Token:
// Axios拦截器示例 axios.interceptors.request.use(config => { const token = sessionStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });
网关添加Token校验逻辑,确保请求合法性:
app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/api")) { var authHeader = context.Request.Headers.Authorization.FirstOrDefault(); if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; await context.Response.WriteAsync("缺少有效授权头"); return; } var token = authHeader.Substring("Bearer ".Length); // 调用身份提供商的Token校验接口,或本地验证JWT签名 var isValid = await ValidateToken(token); if (!isValid) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; await context.Response.WriteAsync("无效Token"); return; } } await next(); }); // 示例:JWT本地验证逻辑 private async Task<bool> ValidateToken(string token) { var validator = context.RequestServices.GetRequiredService<ITokenValidator>(); return await validator.ValidateAsync(token); }
关键说明
- 完全无状态:网关不存储任何用户会话数据,所有认证状态由前端Session Storage维护,无需粘性会话或外部存储。
- 安全性:确保回调页面与前端同域,避免XSS风险;全程使用HTTPS传输Token,防止中间人攻击;Session Storage仅在当前标签页有效,如需跨标签页可改用Local Storage,但需额外防范XSS。
内容的提问来源于stack exchange,提问作者Zach Gardner
相关产品推荐
相关产品推荐

