PAYATHON 2026

.NET 如何配置适用于所有环境的通用 Redirect URL

支付老李

结论

不存在既包含 localhost,又能同时用于开发和生产环境的绝对 URL。localhost 始终指向发起请求的设备,应用部署后必须改用生产环境能够访问的域名。

建议这样处理:

  • 按环境配置支付平台的回调地址。开发环境使用测试地址,生产环境使用正式的 HTTPS 域名。
  • 支付完成后的站内跳转尽量使用相对 URL。
  • 如果前端和 API 使用不同域名,将前端地址也放入配置,并由各环境分别覆盖。
  • 不要使用未经验证的 Host 请求头生成支付回调地址。

为什么 localhost 部署后无法工作

下面的代码会让浏览器访问用户自己电脑上的 5173 端口,而不是生产服务器:

return Redirect("http://localhost:5173/success");

同理,支付服务商也无法通过下面的地址访问已经部署的应用:

"https://localhost:7089/api/Payment/callback-payment"

支付回调通常由支付服务商的服务器发起,所以回调地址必须是对方能够访问的公网 HTTPS 地址。开发环境需要接收真实回调时,可以使用支付平台提供的测试机制,或者经过授权的 HTTPS 隧道服务。具体要求取决于所使用的支付平台。

方案一:按环境覆盖配置

可以先在基础配置中定义结构,但不要把正式域名直接写进控制器。

appsettings.json:

{
  "PaymentCallback": {
    "ReturnUrl": ""
  },
  "Frontend": {
    "BaseUrl": ""
  }
}

开发环境的配置放在 appsettings.Development.json:

{
  "PaymentCallback": {
    "ReturnUrl": "https://localhost:7089/api/Payment/callback-payment"
  },
  "Frontend": {
    "BaseUrl": "http://localhost:5173"
  }
}

生产环境可以使用 appsettings.Production.json:

{
  "PaymentCallback": {
    "ReturnUrl": "https://api.example.com/api/Payment/callback-payment"
  },
  "Frontend": {
    "BaseUrl": "https://www.example.com"
  }
}

ASP.NET Core 会根据 DOTNET_ENVIRONMENT 或 ASPNETCORE_ENVIRONMENT 加载对应环境的配置。例如,环境名是 Production 时,系统会在基础配置之上加载 appsettings.Production.json。

生产域名通常更适合通过环境变量或部署平台的配置中心提供:

PaymentCallback__ReturnUrl=https://api.example.com/api/Payment/callback-payment
Frontend__BaseUrl=https://www.example.com

环境变量中的双下划线 __ 对应配置键的层级分隔符 :。

使用 Options 读取配置

先定义配置类:

public sealed class PaymentCallbackOptions
{
    public string ReturnUrl { get; set; } = string.Empty;
}

public sealed class FrontendOptions
{
    public string BaseUrl { get; set; } = string.Empty;
}

然后在 Program.cs 中注册:

builder.Services
    .AddOptions<PaymentCallbackOptions>()
    .Bind(builder.Configuration.GetSection("PaymentCallback"))
    .Validate(
        options => Uri.TryCreate(
            options.ReturnUrl,
            UriKind.Absolute,
            out var uri) &&
            uri.Scheme == Uri.UriSchemeHttps,
        "PaymentCallback:ReturnUrl must be an absolute HTTPS URL.")
    .ValidateOnStart();

builder.Services
    .AddOptions<FrontendOptions>()
    .Bind(builder.Configuration.GetSection("Frontend"))
    .Validate(
        options => Uri.TryCreate(
            options.BaseUrl,
            UriKind.Absolute,
            out _),
        "Frontend:BaseUrl must be an absolute URL.")
    .ValidateOnStart();

控制器通过依赖注入读取配置:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

public sealed class PaymentController : ControllerBase
{
    private readonly PaymentCallbackOptions _callbackOptions;
    private readonly FrontendOptions _frontendOptions;
    private readonly IPaymentService _paymentService;
    private readonly ILogger<PaymentController> _logger;

    public PaymentController(
        IOptions<PaymentCallbackOptions> callbackOptions,
        IOptions<FrontendOptions> frontendOptions,
        IPaymentService paymentService,
        ILogger<PaymentController> logger)
    {
        _callbackOptions = callbackOptions.Value;
        _frontendOptions = frontendOptions.Value;
        _paymentService = paymentService;
        _logger = logger;
    }

    [HttpGet("callback-payment")]
    public async Task<IActionResult> PaymentCallback()
    {
        _logger.LogInformation("Start save bill");

        var response = await _paymentService.PaymentExecute(Request.Query);

        var successUrl = new Uri(
            new Uri(_frontendOptions.BaseUrl.TrimEnd('/') + "/"),
            "success");

        return Redirect(successUrl.ToString());
    }
}

创建支付请求时,需要把 PaymentCallback:ReturnUrl 传给支付服务商,不能只把它写在配置文件中:

var paymentRequest = new PaymentRequest
{
    ReturnUrl = _callbackOptions.ReturnUrl
};

var result = await _paymentService.CreatePayment(paymentRequest);

具体属性名称取决于支付 SDK。这里的 PaymentRequest 和 ReturnUrl 只是结构示例。

方案二:同一站点使用相对跳转

如果 /success 页面和当前 API 位于同一域名,可以直接使用相对路径:

return Redirect("/success");

浏览器会保留当前请求使用的协议和主机名。例如:

  • 开发环境可能跳转到 https://localhost:7089/success
  • 生产环境可能跳转到 https://example.com/success

如果应用使用 MVC 或 Razor Pages,优先通过路由生成跳转地址:

return RedirectToAction("Success", "Payment");

或者:

return RedirectToPage("/Payment/Success");

这样可以避免在业务代码中硬编码路由路径。如果前端运行在独立域名或端口,例如 API 位于 api.example.com,前端位于 www.example.com,相对路径就不够用了,需要配置前端基础地址。

动态生成支付回调 URL

如果支付 SDK 要求使用绝对 URL,也可以根据 ASP.NET Core 路由生成:

var callbackUrl = Url.Action(
    action: nameof(PaymentCallback),
    controller: "Payment",
    values: null,
    protocol: Request.Scheme);

这种方式只有在应用能够正确识别外部协议和主机名时才可靠。应用部署在 Nginx、Ingress、负载均衡器或反向代理后面时,内部请求可能显示为 HTTP,而外部实际使用的是 HTTPS。这时需要正确配置代理并启用转发头处理。

示意配置如下:

using Microsoft.AspNetCore.HttpOverrides;

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor |
        ForwardedHeaders.XForwardedProto |
        ForwardedHeaders.XForwardedHost;

    // 应根据实际部署环境配置 KnownProxies 或 KnownNetworks。
});

var app = builder.Build();

app.UseForwardedHeaders();

不能为了方便而无条件信任任意客户端发送的转发头。应根据实际部署拓扑明确配置可信代理、网络范围和代理覆盖行为,否则生成的 URL 可能被伪造。

对支付回调来说,显式配置公开 URL 通常更稳妥,因为支付平台往往还要求在管理后台预先登记允许使用的回调域名。

安全和部署注意事项

  • 生产回调地址应使用 HTTPS,并确保证书有效。
  • 回调接口应验证支付平台提供的签名、事件标识或其他真实性凭据,不能只根据查询参数确认付款成功。
  • 支付平台可能重复发送回调,因此支付处理逻辑需要具备幂等性。
  • 不要把请求参数中的 returnUrl 直接传给 Redirect(),否则可能造成开放重定向漏洞。
  • 如果确实需要支持站内动态跳转,应验证目标是否为本地 URL:
if (!Url.IsLocalUrl(returnUrl))
{
    return BadRequest("Invalid return URL.");
}

return LocalRedirect(returnUrl);
  • 不要把密钥等敏感数据提交到 appsettings.Production.json。域名本身通常不是秘密,但统一通过环境变量或配置中心管理,更便于部署。
  • 开发、测试和生产环境可能分别使用支付平台中的不同应用、密钥和回调白名单,需要单独配置。

要解决的不是如何找到一个适用于所有环境的 URL,而是让代码保持一致,由各环境提供自己的公开地址。只有跳转到同域名下的内部页面时,相对 URL 才能真正实现跨环境复用。

备注:内容仅供参考。