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

ASP.NET Core中间件Windows认证问题:访问/ecp路径持续重定向至登录页

ASP.NET Core代理中间件无法传递Windows凭据导致/ecp路径持续重定向至登录页

问题背景

  • 部署环境:IIS托管的ASP.NET Core应用,启用Windows认证,禁用Forms认证
  • 核心现象:自定义代理中间件转发/ecp路径请求到下游服务器时,持续重定向至登录页
  • 已尝试操作:设置HttpClientHandler.UseDefaultCredentials = true以传递Windows凭据,检查请求头未发现异常

中间件代码

using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace ECP.Web.Configurations.Middleware
{
    public static class Routing
    {
        public static void Apply(WebApplication app)
        {
            app.UseRouting();

            app.UseMiddleware<SimpleProxyMiddleware>();

            // Routing dla CommonController
            app.MapControllerRoute(
                name: "ecpRoute",
                pattern: "ecp/{action=Index}",
                defaults: new { controller = "Common", action = "Index" });  

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Common}/{action=Index}");

            app.MapRazorPages();
        }
    }

    public class SimpleProxyMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly HttpClient _httpClient;
        private readonly ILogger<SimpleProxyMiddleware> _logger;

        public SimpleProxyMiddleware(RequestDelegate next, ILogger<SimpleProxyMiddleware> logger)
        {
            _next = next;

            var handler = new HttpClientHandler
            {
                UseDefaultCredentials = true,  // This setting passes Windows Authentication credentials
                AllowAutoRedirect = false
            };

            _httpClient = new HttpClient(handler);
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var upstreamPath = context.Request.Path.Value;

            // Check if the request has already been processed by this middleware
            if (context.Request.Headers.ContainsKey("X-Proxy-Processed"))
            {
                _logger.LogInformation("Request already processed by proxy, passing to next middleware.");
                await _next(context);
                return;
            }

            if (upstreamPath.StartsWith("/ecp"))
            {
                // Modify the URL to point to the downstream service
                var downstreamUrl = $"https://ecp.dev.ad.ulcp:44392{upstreamPath}";

                _logger.LogInformation("Requesting URL: {DownstreamUrl}", downstreamUrl);

                var requestMessage = new HttpRequestMessage
                {
                    Method = new HttpMethod(context.Request.Method),
                    RequestUri = new Uri(downstreamUrl)
                };

                // Mark the request as processed by this proxy
                requestMessage.Headers.Add("X-Proxy-Processed", "true");

                // Copy request headers, including authentication information
                _logger.LogInformation("Request Headers:");
                foreach (var header in context.Request.Headers)
                {
                    _logger.LogInformation("{HeaderKey}: {HeaderValue}", header.Key, string.Join(", ", header.Value));
                    requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                }

                try
                {
                    // Send the request to the downstream service
                    var responseMessage = await _httpClient.SendAsync(requestMessage);

                    context.Response.StatusCode = (int)responseMessage.StatusCode;

                    _logger.LogInformation("Response Status Code: {StatusCode}", responseMessage.StatusCode);

                    // Copy response headers
                    _logger.LogInformation("Response Headers:");
                    foreach (var header in responseMessage.Headers)
                    {
                        _logger.LogInformation("{HeaderKey}: {HeaderValue}", header.Key, string.Join(", ", header.Value));
                        context.Response.Headers[header.Key] = header.Value.ToArray();
                    }

                    foreach (var header in responseMessage.Content.Headers)
                    {
                        _logger.LogInformation("{HeaderKey}: {HeaderValue}", header.Key, string.Join(", ", header.Value));
                        context.Response.Headers[header.Key] = header.Value.ToArray();
                    }

                    context.Response.Headers.Remove("transfer-encoding");

                    // Forward the response content
                    await responseMessage.Content.CopyToAsync(context.Response.Body);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "An error occurred while processing the request.");
                    throw;
                }
            }
            else
            {
                await _next(context);
            }
        }
    }
}

IIS配置

  • 已启用Windows认证
  • 已禁用Forms认证

Ocelot配置(备用代理方案)

{
  "DownstreamPathTemplate": "/ecp/{url}",
  "DownstreamScheme": "https",
  "DownstreamHostAndPorts": [
    {
      "Host": "ecp.dev.ad.uclp", 
      "Port": 443
    }
  ],
  "UpstreamPathTemplate": "/ecp/{url}",
  "UpstreamHttpMethod": [ "Get" ]
}

问题分析与解决方案

核心原因

  1. 中间件顺序错误:代理中间件在UseRouting之后执行,而路由规则中已定义ecpRoute,导致/ecp请求先被路由到CommonController,未进入代理中间件处理。
  2. 凭据传递逻辑错误:UseDefaultCredentials = true传递的是应用池身份的凭据,而非当前登录用户的Windows凭据,下游服务器因身份不匹配返回登录重定向。
  3. 重定向响应未处理:下游返回的302重定向直接被转发,重定向目标为下游登录页,且未保留原用户认证上下文,导致循环重定向。

解决方案

1. 调整中间件顺序

将代理中间件移至UseRouting之前,确保/ecp请求先被代理捕获:

public static void Apply(WebApplication app)
{
    // 优先执行代理中间件
    app.UseMiddleware<SimpleProxyMiddleware>();

    app.UseRouting();

    // 后续路由配置保持不变
    app.MapControllerRoute(
        name: "ecpRoute",
        pattern: "ecp/{action=Index}",
        defaults: new { controller = "Common", action = "Index" });  

    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Common}/{action=Index}");

    app.MapRazorPages();
}

2. 正确传递用户Windows凭据(Kerberos委派)

若需转发当前用户的Windows凭据,需配置Kerberos约束委派:

  • 在AD中为应用池的服务账户配置对下游服务器服务的约束委派权限
  • 在代理中间件中临时 impersonate 当前用户身份发送请求:
// 在InvokeAsync方法内获取当前用户Windows身份
var windowsIdentity = context.User.Identity as WindowsIdentity;
if (windowsIdentity != null)
{
    using (windowsIdentity.Impersonate())
    {
        var handler = new HttpClientHandler
        {
            UseDefaultCredentials = true,
            AllowAutoRedirect = false
        };
        // 使用临时HttpClient在 impersonation 上下文发送请求
        using (var client = new HttpClient(handler))
        {
            var responseMessage = await client.SendAsync(requestMessage);
            // 后续响应处理逻辑...
        }
    }
}

注意:不要在中间件构造函数中初始化HttpClient,需在InvokeAsync内根据当前用户身份创建,避免凭据复用冲突。

3. 处理重定向响应

当下游返回302重定向时,修改重定向目标为本应用路径:

if (responseMessage.StatusCode == HttpStatusCode.Redirect || 
    responseMessage.StatusCode == HttpStatusCode.Found)
{
    var redirectUrl = responseMessage.Headers.Location.ToString();
    // 替换下游域名为本应用域名
    var newRedirectUrl = redirectUrl.Replace("https://ecp.dev.ad.ulcp:44392", $"{context.Request.Scheme}://{context.Request.Host}");
    context.Response.Redirect(newRedirectUrl);
    return;
}

4. 清理冲突请求头

转发请求时移除可能导致认证问题的头信息:

// 复制请求头前跳过冲突字段
foreach (var header in context.Request.Headers)
{
    if (header.Key.Equals("Connection", StringComparison.OrdinalIgnoreCase) || 
        header.Key.Equals("Host", StringComparison.OrdinalIgnoreCase))
    {
        continue;
    }
    requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 03:42:32