You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core 6无状态微服务:无Cookie的OAuth2/OpenIDConnect实现方案

解决方案:自定义OIDC回调处理,绕过Cookie会话存储

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 13:56:10