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

.NET 4.8 Owin OpenIdAuthentication:正式页面不返回302跳转问题

问题描述

为基于.NET Framework 4.8的老旧Web应用接入OpenID认证时,遇到以下异常情况:

  • 测试页Claims.aspx(继承自System.Web.UI.Page)调用HttpContext.Current.GetOwinContext().Authentication.Challenge()可正常触发302重定向到Keycloak完成授权流程。
  • 将相同认证逻辑迁移到继承自AuthPage的正式页面后,页面会继续加载,无法触发302跳转。
  • AuthPage已替换原Forms认证逻辑为OpenID的Challenge调用,但问题依旧。根据Challenge方法说明,该方法应向响应环境添加认证挑战信息,将状态码改为401或转为302重定向,但正式页面未触发该行为。

需求:在尽可能少改动现有应用代码的前提下解决问题。


相关代码

Startup.cs

public void ConfigureAuth(IAppBuilder app)
{
    var notificationHandlers = new OpenIdConnectAuthenticationNotifications
    {
        AuthorizationCodeReceived = async (context) => {
            // Sign in the user here
        },
        RedirectToIdentityProvider = (context) => {
            if (context.OwinContext.Request.Path.Value != "/Account/SignInWithOpenId")
            {
                context.OwinContext.Response.Redirect("/Account/Login");
                context.HandleResponse();
            }
            return Task.FromResult(0);
        }
    };

    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        ClientId = _clientId,
        ClientSecret = _clientSecret,
        Authority = _authority,
        RedirectUri = _redirectUri,
        ResponseType = OpenIdConnectResponseType.CodeIdToken,
        Scope = OpenIdConnectScope.OpenIdProfile,
        TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", },
        AuthenticationMode = AuthenticationMode.Active,
        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            AuthorizationCodeReceived = async n =>
            {
                // Exchange code for access and ID tokens
                var tokenClientOptions = new TokenClientOptions
                {
                    ClientId = _clientId,
                    ClientSecret = _clientSecret,
                    Address = $"{_authority}/protocol/openid-connect/token"

                };

                var tokenClient = new TokenClient(new HttpClient() { BaseAddress = new Uri($"{_authority}/protocol/openid-connect/token") }, tokenClientOptions);

                var client = new HttpClient();
                var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
                {
                    Address = $"{_authority}/protocol/openid-connect/token",
                    ClientId = _clientId,
                    ClientSecret = _clientSecret,
                    Code = n.Code,
                    RedirectUri = _redirectUri,
                });

                var client2 = new HttpClient();

                var userInfoResponse = await client2.GetUserInfoAsync(new UserInfoRequest
                {
                    Address = $"{_authority}protocol/openid-connect/userinfo",
                    Token = tokenResponse.AccessToken
                });

                var claims = new List<Claim>(userInfoResponse.Claims)
                  {
                    new Claim("id_token", tokenResponse.IdentityToken),
                    new Claim("access_token", tokenResponse.AccessToken)
                  };

                n.AuthenticationTicket.Identity.AddClaims(claims);
            },

            RedirectToIdentityProvider = notification =>
            {
                if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                {
                    var logoutUri = $"https://{_redirectUri}/v2/logout?client_id={_clientId}";

                    var postLogoutUri = notification.ProtocolMessage.PostLogoutRedirectUri;
                    if (!string.IsNullOrEmpty(postLogoutUri))
                    {
                        if (postLogoutUri.StartsWith("/"))
                        {
                            // transform to absolute
                            var request = notification.Request;
                            postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
                        }
                        logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
                    }

                    notification.Response.Redirect(logoutUri);
                    notification.HandleResponse();
                }

                notification.OwinContext.Response.Redirect("/Account/Logiasdfn");
                notification.HandleResponse();

                return Task.FromResult(0);
            }
        },
            });

            // Set Cookies as default authentication type
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
                LoginPath = new PathString("/teset.aspx"),
                CookieSameSite = SameSiteMode.Lax,
                // More information on why the CookieManager needs to be set can be found here: 
                // https://github.com/aspnet/AspNetKatana/wiki/System.Web-response-cookie-integration-issues
                CookieManager = new SameSiteCookieManager(new Microsoft.Owin.Host.SystemWeb.SystemWebCookieManager())
            });
        }
}

Claims.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/MasterMainTest.master" AutoEventWireup="true" CodeBehind="Claims.aspx.cs" Inherits="RootNamespace.Web.Claims" %>
<%@ Import Namespace="System.Security.Claims" %>

<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder23" runat="server">
    <h2>OpenID Connect Claims</h2>
    <dl>
        <asp:DataList runat="server" ID="dlClaims">
            <ItemTemplate>
                <dt><%# ((Claim) Container.DataItem).Type %></dt>
                <dd><%# ((Claim) Container.DataItem).Value %></dd>
            </ItemTemplate>
        </asp:DataList>
    </dl>
</asp:Content>

Claims.aspx.cs

public partial class Claims : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Request.IsAuthenticated)
        {
            HttpContext.Current.GetOwinContext().Authentication.Challenge();
        }

        var claims = ClaimsPrincipal.Current.Claims;
        dlClaims.DataSource = claims;
        dlClaims.DataBind();
    }
}

AuthPage.cs

private void CheckSession(object sender, EventArgs ea)
{
    /*OpenID authentication */
    if (!Request.IsAuthenticated)
    {
        HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "~/test.aspx", AllowRefresh = true, IsPersistent = true });
    }

    /* Forms authentication */ /*
    object o = Session["Admin"];

    if ((o is Administrator) == false)
    {
        // OK, not logged in, save the address of this page 
        // so we can return to it if/when the admin logs in
        Session["GoToWhenLoggedIn"] = Request.Url.PathAndQuery;
        Response.Redirect(Config.LoginPageName);
    } */
}

补充代码

public static void UseExternalSignInCookie(this IAppBuilder app)
{
    UseExternalSignInCookie(app, DefaultAuthenticationTypes.ExternalCookie);
}

public static void UseExternalSignInCookie(this IAppBuilder app, string externalAuthenticationType)
{
    if (app == null)
    {
        throw new ArgumentNullException("app");
    }

    app.SetDefaultSignInAsAuthenticationType(externalAuthenticationType);
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = externalAuthenticationType,
        AuthenticationMode = AuthenticationMode.Active,
        CookieName = CookiePrefix + externalAuthenticationType,
        ExpireTimeSpan = TimeSpan.FromMinutes(5),
    });
}

解决方案

1. 修复Startup.cs中RedirectToIdentityProvider的错误逻辑

当前OpenIdConnectAuthenticationOptions的RedirectToIdentityProvider处理存在两个致命问题:

  • 非登出请求时,硬编码重定向到不存在的路径/Account/Logiasdfn,并调用HandleResponse()终止正常OpenID流程。
  • 定义的notificationHandlers变量未被使用,属于冗余代码。

修改后的处理逻辑:

RedirectToIdentityProvider = notification =>
{
    if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
    {
        var logoutUri = $"{_authority}/protocol/openid-connect/logout?client_id={_clientId}";

        var postLogoutUri = notification.ProtocolMessage.PostLogoutRedirectUri;
        if (!string.IsNullOrEmpty(postLogoutUri))
        {
            if (postLogoutUri.StartsWith("/"))
            {
                var request = notification.Request;
                postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
            }
            logoutUri += $"&redirect_uri={ Uri.EscapeDataString(postLogoutUri)}";
        }

        notification.Response.Redirect(logoutUri);
        notification.HandleResponse();
    }
    // 非登出请求不拦截,让OpenID中间件自动处理重定向到Keycloak
    return Task.FromResult(0);
}

同时删除未使用的notificationHandlers变量,减少冗余。

2. 确保AuthPage的认证检查时机正确

CheckSession方法需绑定到页面早期事件(如PreInit),若绑定到Load事件之后,页面可能已开始输出内容,导致重定向失效。调整AuthPage代码:

public class AuthPage : System.Web.UI.Page
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        PreInit += CheckSession; // 在页面初始化早期触发认证检查
    }

    private void CheckSession(object sender, EventArgs ea)
    {
        if (!Request.IsAuthenticated)
        {
            // 使用当前请求路径作为跳转后返回地址,避免硬编码
            var redirectUri = Request.Url.PathAndQuery;
            HttpContext.Current.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = redirectUri }
            );
            Response.End(); // 终止页面后续处理,确保重定向生效
        }
    }
}

3. 修正CookieAuthenticationOptions的LoginPath拼写错误

当前LoginPath设置为/teset.aspx(拼写错误),修正为正确路径:

LoginPath = new PathString("/test.aspx"),

4. 调整Owin中间件加载顺序

中间件加载顺序必须遵循:Cookie认证 → 外部登录Cookie → OpenID认证。调整后的ConfigureAuth片段:

// Set Cookies as default authentication type
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

// 先加载Cookie认证中间件
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
    LoginPath = new PathString("/test.aspx"),
    CookieSameSite = SameSiteMode.Lax,
    CookieManager = new SameSiteCookieManager(new Microsoft.Owin.Host.SystemWeb.SystemWebCookieManager())
});

// 再加载外部登录Cookie
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

// 最后加载OpenID认证中间件
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    // 其他配置...
});

5. 避免Response提前写入内容

若AuthPage或母版页在CheckSession前已向Response写入内容,重定向会失效。可在CheckSession中添加检查:

private void CheckSession(object sender, EventArgs ea)
{
    if (!Request.IsAuthenticated)
    {
        if (!Response.IsRequestBeingRedirected && Response.ContentLength == 0)
        {
            var redirectUri = Request.Url.PathAndQuery;
            HttpContext.Current.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = redirectUri }
            );
            Response.End();
        }
    }
}

内容的提问来源于stack exchange,提问作者Sputnikk23

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 16:54:53