ASP.NET Core 中的重定向(Redirect)深度解析

重定向是 Web 开发中最基础却又最容易被误用的机制之一。一个用错的状态码可能导致表单重复提交、SEO(Search Engine Optimization,搜索引擎优化)权重丢失,甚至打开开放重定向(Open Redirect)漏洞。本文从 HTTP 协议层出发,逐层剖析 ASP.NET Core 在 MVC(Model-View-Controller)控制器、Razor Pages 与 Minimal API(最小化 API)三种编程模型下的重定向能力,并落到安全实践与源码级行为上。本文基于 ASP.NET Core in .NET 10。


一、协议基础:四个重定向状态码

ASP.NET Core 所有重定向 API 最终都归结为往响应里写一个 3xx 状态码加上一个 Location 响应头。真正需要理解清楚的,是下面四个状态码的语义差异------它们由两个正交的布尔维度组合而成:

状态码 名称 是否永久(permanent) 是否保留请求方法(preserveMethod)
302 Found
301 Moved Permanently
307 Temporary Redirect
308 Permanent Redirect

这两个维度的含义是理解全部重定向 API 的钥匙:

permanent(永久 vs 临时) 决定的是缓存与 SEO 语义。301/308 告诉浏览器和搜索引擎"这个资源永久搬家了",浏览器会缓存该结果,搜索引擎会把权重转移到新地址。302/307 表示"暂时去那边,但请继续用原地址访问"。用错方向的代价不对称:错误地发 301 会被客户端长期缓存,事后极难纠正;不确定时应优先选 302。

preserveMethod(是否保留方法与请求体) 是 301/302 与 307/308 的核心分水岭,也是最容易被忽视的点。历史上 301/302 存在一个长期的实现偏差:当浏览器收到对一个 POST 请求的 301/302 响应时,往往会把后续请求降级为 GET 并丢弃请求体。这正是 PRG(Post-Redirect-Get,提交后重定向到 Get)模式赖以工作的基础。而 307/308 则严格要求客户端用原始的方法和请求体 重新发起请求------POST 仍然是 POST,请求体原样带上。
#mermaid-svg-RVaCF2ygaQfehHOe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RVaCF2ygaQfehHOe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RVaCF2ygaQfehHOe .error-icon{fill:#552222;}#mermaid-svg-RVaCF2ygaQfehHOe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RVaCF2ygaQfehHOe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RVaCF2ygaQfehHOe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RVaCF2ygaQfehHOe .marker.cross{stroke:#333333;}#mermaid-svg-RVaCF2ygaQfehHOe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RVaCF2ygaQfehHOe p{margin:0;}#mermaid-svg-RVaCF2ygaQfehHOe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RVaCF2ygaQfehHOe .cluster-label text{fill:#333;}#mermaid-svg-RVaCF2ygaQfehHOe .cluster-label span{color:#333;}#mermaid-svg-RVaCF2ygaQfehHOe .cluster-label span p{background-color:transparent;}#mermaid-svg-RVaCF2ygaQfehHOe .label text,#mermaid-svg-RVaCF2ygaQfehHOe span{fill:#333;color:#333;}#mermaid-svg-RVaCF2ygaQfehHOe .node rect,#mermaid-svg-RVaCF2ygaQfehHOe .node circle,#mermaid-svg-RVaCF2ygaQfehHOe .node ellipse,#mermaid-svg-RVaCF2ygaQfehHOe .node polygon,#mermaid-svg-RVaCF2ygaQfehHOe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RVaCF2ygaQfehHOe .rough-node .label text,#mermaid-svg-RVaCF2ygaQfehHOe .node .label text,#mermaid-svg-RVaCF2ygaQfehHOe .image-shape .label,#mermaid-svg-RVaCF2ygaQfehHOe .icon-shape .label{text-anchor:middle;}#mermaid-svg-RVaCF2ygaQfehHOe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RVaCF2ygaQfehHOe .rough-node .label,#mermaid-svg-RVaCF2ygaQfehHOe .node .label,#mermaid-svg-RVaCF2ygaQfehHOe .image-shape .label,#mermaid-svg-RVaCF2ygaQfehHOe .icon-shape .label{text-align:center;}#mermaid-svg-RVaCF2ygaQfehHOe .node.clickable{cursor:pointer;}#mermaid-svg-RVaCF2ygaQfehHOe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RVaCF2ygaQfehHOe .arrowheadPath{fill:#333333;}#mermaid-svg-RVaCF2ygaQfehHOe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RVaCF2ygaQfehHOe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RVaCF2ygaQfehHOe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RVaCF2ygaQfehHOe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RVaCF2ygaQfehHOe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RVaCF2ygaQfehHOe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RVaCF2ygaQfehHOe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RVaCF2ygaQfehHOe .cluster text{fill:#333;}#mermaid-svg-RVaCF2ygaQfehHOe .cluster span{color:#333;}#mermaid-svg-RVaCF2ygaQfehHOe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RVaCF2ygaQfehHOe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RVaCF2ygaQfehHOe rect.text{fill:none;stroke-width:0;}#mermaid-svg-RVaCF2ygaQfehHOe .icon-shape,#mermaid-svg-RVaCF2ygaQfehHOe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RVaCF2ygaQfehHOe .icon-shape p,#mermaid-svg-RVaCF2ygaQfehHOe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RVaCF2ygaQfehHOe .icon-shape .label rect,#mermaid-svg-RVaCF2ygaQfehHOe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RVaCF2ygaQfehHOe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RVaCF2ygaQfehHOe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RVaCF2ygaQfehHOe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 临时
永久
否,允许降级为GET

否,允许降级为GET

收到重定向请求
资源是否永久迁移?
是否必须保留

HTTP方法和请求体?
是否必须保留

HTTP方法和请求体?
302 Found
307 Temporary Redirect
301 Moved Permanently
308 Permanent Redirect

经验法则:浏览器导航跳转、PRG 防重复提交,用 302(默认);API 之间需要把 POST/PUT 原样转发,用 307/308;站点域名/路径永久搬迁,用 301(GET 场景)或 308(需保方法)。


二、底层原语:HttpResponse.Redirect

无论上层用什么模型,最底层的写入点都是 Microsoft.AspNetCore.Http 命名空间里的 ResponseExtensions.Redirect 扩展方法。它直接操作 HttpResponse,没有任何路由解析或安全校验:

csharp 复制代码
public static void Redirect(
    this HttpResponse response,
    string location,
    bool permanent,
    bool preserveMethod);

参数语义与上一节的表格完全一一对应:permanenttrue 时是 301 或 308,preserveMethodtrue 时是 307 或 308。location 必须是已经正确编码、只含 ASCII 字符的字符串,因为它要被直接塞进 HTTP 响应头。

这是所有重定向的"汇流处"。理解了它,上层那些名目繁多的 RedirectXxx 方法本质上都只是在帮你计算出 location 这个字符串,再调用它而已。在中间件(Middleware)中需要做重定向时,通常直接调用这一层。


三、MVC 控制器中的重定向

在继承自 ControllerBase / Controller 的控制器里,重定向通过返回 IActionResult 来表达。这些辅助方法可以按"目标如何指定"分成三类,每一类又都有"永久"和"保留方法"两个变体。

3.1 三类目标 × 四种结果类型

第一类:重定向到原始 URL 字符串 ------ Redirect

csharp 复制代码
public IActionResult Go() => Redirect("/products/42");

Redirect(url) 返回一个 RedirectResult。该结果类型可产生 302/301/307/308 中的任意一个,附带指向所给 URL 的 Location 头。其构造函数同样暴露了底层的两个布尔维度:

csharp 复制代码
public RedirectResult(string url, bool permanent, bool preserveMethod);

对应的语义化辅助方法:

方法 状态码
Redirect(url) 302
RedirectPermanent(url) 301
RedirectPreserveMethod(url) 307
RedirectPermanentPreserveMethod(url) 308

第二类:重定向到某个控制器动作(Action)------ RedirectToAction

csharp 复制代码
return RedirectToAction(nameof(HomeController.Index), "Home", new { id = 42 });

这会返回 RedirectToActionResult。它不直接接受 URL,而是接受动作名、控制器名和路由值(route values),由框架在执行时通过 IUrlHelper 反向生成 URL。它的构造函数完整暴露了四种状态码:

csharp 复制代码
public RedirectToActionResult(
    string? actionName, string? controllerName,
    object? routeValues, bool permanent, bool preserveMethod);

第三类:重定向到一条命名路由(Named Route)------ RedirectToRoute

csharp 复制代码
return RedirectToRoute("orderDetails", new { orderId = 42 });

返回 RedirectToRouteResult,靠路由名称而非动作名来生成 URL,适合路由结构和控制器结构解耦的场景。

每一类都有完整的四方法矩阵(基础版、PermanentPreserveMethodPermanentPreserveMethod),命名规律完全一致,不再赘述。

3.2 一个常被忽视的细节:IKeepTempDataResult

RedirectResultRedirectToActionResultRedirectToRouteResult 都实现了 IKeepTempDataResult 接口。这个接口的语义是:在该结果执行期间,TempData 不会被标记为已读、不会被清除 。这正是 PRG 模式下能把"操作成功"提示消息从 POST 动作带到重定向后的 GET 页面的底层机制------TempData 默认是"读取后即清除",而重定向结果会保留它跨过这一次跳转。

3.3 执行链路

RedirectResult 为例,它本身只是个数据载体。真正干活的是基础设施层的执行器(Executor),通过 IActionResultExecutor<RedirectResult> 在请求管线中被解析并调用,最终落到第二节的 HttpResponse.Redirect 上。LocalRedirectResult 对应的是 LocalRedirectResultExecutor.ExecuteAsync。需要注意旧的同步 ExecuteResult(ActionContext) 路径已标记为 [Obsolete],框架内部统一走 ExecuteResultAsync
HttpResponse.Redirect IActionResultExecutor RedirectResult 控制器动作 HttpResponse.Redirect IActionResultExecutor RedirectResult 控制器动作 #mermaid-svg-mofsY6AjZH4LkhDU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mofsY6AjZH4LkhDU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mofsY6AjZH4LkhDU .error-icon{fill:#552222;}#mermaid-svg-mofsY6AjZH4LkhDU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mofsY6AjZH4LkhDU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mofsY6AjZH4LkhDU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mofsY6AjZH4LkhDU .marker.cross{stroke:#333333;}#mermaid-svg-mofsY6AjZH4LkhDU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mofsY6AjZH4LkhDU p{margin:0;}#mermaid-svg-mofsY6AjZH4LkhDU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mofsY6AjZH4LkhDU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-mofsY6AjZH4LkhDU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mofsY6AjZH4LkhDU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-mofsY6AjZH4LkhDU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-mofsY6AjZH4LkhDU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-mofsY6AjZH4LkhDU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-mofsY6AjZH4LkhDU .sequenceNumber{fill:white;}#mermaid-svg-mofsY6AjZH4LkhDU #sequencenumber{fill:#333;}#mermaid-svg-mofsY6AjZH4LkhDU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-mofsY6AjZH4LkhDU .messageText{fill:#333;stroke:none;}#mermaid-svg-mofsY6AjZH4LkhDU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mofsY6AjZH4LkhDU .labelText,#mermaid-svg-mofsY6AjZH4LkhDU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-mofsY6AjZH4LkhDU .loopText,#mermaid-svg-mofsY6AjZH4LkhDU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-mofsY6AjZH4LkhDU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mofsY6AjZH4LkhDU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-mofsY6AjZH4LkhDU .noteText,#mermaid-svg-mofsY6AjZH4LkhDU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-mofsY6AjZH4LkhDU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mofsY6AjZH4LkhDU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mofsY6AjZH4LkhDU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mofsY6AjZH4LkhDU .actorPopupMenu{position:absolute;}#mermaid-svg-mofsY6AjZH4LkhDU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-mofsY6AjZH4LkhDU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mofsY6AjZH4LkhDU .actor-man circle,#mermaid-svg-mofsY6AjZH4LkhDU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-mofsY6AjZH4LkhDU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} return Redirect("/x")框架解析并调用 ExecuteResultAsync写入 Location 头 + 3xx 状态码


四、Razor Pages 中的重定向

Razor Pages 在 PageModel 上提供了与控制器高度对称的 API。除了复用 Redirect / RedirectPermanent 等之外,最常用的是面向页面的版本:

csharp 复制代码
public IActionResult OnPost()
{
    // ...保存数据...
    return RedirectToPage("./Confirmation", new { id = orderId });
}

RedirectToPage / RedirectToPagePermanent 等方法以 Razor 页面的相对/绝对路径为目标生成 URL,是 Razor Pages 下实现 PRG 模式的标准写法:OnPost 处理完写操作后重定向到一个 OnGet 页面,避免用户刷新时重复提交表单。


五、Minimal API 中的重定向

Minimal API 通过返回 IResult 来描述响应,重定向由静态类 ResultsTypedResults 提供。

csharp 复制代码
app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Results.Redirect 的完整签名同样是熟悉的两个布尔维度:

csharp 复制代码
public static IResult Redirect(
    string url, bool permanent = false, bool preserveMethod = false);

此外还有 Results.RedirectToRoute(按命名路由生成)和 Results.LocalRedirect(见下一节)。对应的结果实现类型位于 Microsoft.AspNetCore.Http.HttpResults 命名空间,如 RedirectToRouteHttpResult

TypedResults 优于 Results

官方明确推荐在 Minimal API 中优先使用 TypedResults 而非 Results 。两者提供的辅助方法集几乎一致,区别在于返回类型:Results.Xxx 一律返回宽泛的 IResult,而 TypedResults.Xxx 返回具体的实现类型。这带来两个实际收益:一是强类型对象更利于单元测试 (可直接做类型断言,无需转型),二是具体类型会自动向 OpenAPI 提供响应元数据 来描述端点。当一个端点可能返回多种结果时,配合 Results<TResult1, TResultN> 联合返回类型使用,还能获得编译期检查------返回了未声明的类型会直接编译报错。


六、安全:防御开放重定向攻击

这是关于重定向最重要 的一节。开放重定向(Open Redirect)漏洞的成因是:应用根据用户可控的输入(通常是 querystring 里的 returnUrl)来决定跳转目标,却不加校验。攻击者构造一个指向你站点、但 returnUrl 指向钓鱼站的链接,用户看到的是可信域名,点击后却被弹到恶意站点------常被用于钓鱼和窃取凭据。

核心原则:把所有用户提供的数据都视为不可信。 如果跳转目标来自 URL 内容,必须确保它只能指向本站(本地 URL),或一个已知的白名单地址。

6.1 LocalRedirect:首选方案

控制器基类提供 LocalRedirect 辅助方法,行为与 Redirect 完全一致,唯一区别是当传入非本地 URL 时它会直接抛异常

csharp 复制代码
public IActionResult SomeAction(string redirectUrl)
{
    return LocalRedirect(redirectUrl);
}

它同样有 LocalRedirectPermanent(301)、LocalRedirectPreserveMethod(307)、LocalRedirectPermanent­PreserveMethod(308)等变体,底层返回 LocalRedirectResult。Minimal API 侧对应 Results.LocalRedirect(localUrl, permanent, preserveMethod)

一个典型且权威的应用场景是 Blazor 的文化(culture)切换:一个控制器把用户选择的语言写入 Cookie,再重定向回原始 URI。官方示例在这里特意使用 LocalRedirect 而非 Redirect ,正是因为 redirectUri 来自请求参数、不可信:

csharp 复制代码
[Route("[controller]/[action]")]
public class CultureController : Controller
{
    public IActionResult Set(string culture, string redirectUri)
    {
        if (culture != null)
        {
            HttpContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(
                    new RequestCulture(culture, culture)));
        }
        return LocalRedirect(redirectUri); // 防开放重定向
    }
}

6.2 IsLocalUrl:先校验后跳转

如果你希望在非本地 URL 时优雅降级(而不是抛异常),可以先用 Url.IsLocalUrl 显式判断:

csharp 复制代码
private IActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    else
    {
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }
}

在 Minimal API 中,则使用静态方法 RedirectHttpResult.IsLocalUrl(url)

csharp 复制代码
if (RedirectHttpResult.IsLocalUrl(url))
{
    return Results.LocalRedirect(url);
}

6.3 "本地 URL"的判定规则

一个 URL 被认定为"本地",需满足以下条件:

  1. 不包含 host(主机)或 authority(授权)部分 ------也就是说不能是 https://evil.com/... 这种带域名的绝对 URL。
  2. 拥有一条绝对路径 (以 / 开头)。
  3. 使用虚拟路径语法 ~/ 的 URL 也算本地。

据此,/products/42~/home/index 是本地的;而 https://evil.com//evil.com(协议相对 URL,极易被忽视)则不是。

6.4 防御建议小结

  • 任何由用户输入决定的跳转,默认使用 LocalRedirect 或先经 IsLocalUrl 校验。
  • 当出现"本应是本地 URL 却收到了非本地 URL"的情况时,记录该 URL 的细节,有助于诊断潜在的重定向攻击。
  • 警惕协议相对 URL(//host)和编码绕过;优先依赖框架的 IsLocalUrl 而非自己手写正则判断。

七、决策速查

场景 推荐 API(MVC 控制器) 推荐 API(Minimal API) 状态码
PRG 防表单重复提交 RedirectToAction / RedirectToPage --- 302
跳转到用户提供的 returnUrl LocalRedirect Results.LocalRedirect 302
站点路径永久搬迁(GET) RedirectPermanent Results.Redirect(url, permanent:true) 301
API 间转发,需保留 POST 与请求体 RedirectPreserveMethod Results.Redirect(url, preserveMethod:true) 307
永久搬迁且需保留方法 RedirectPermanentPreserveMethod Results.Redirect(url, true, true) 308
中间件中直接重定向 HttpResponse.Redirect HttpResponse.Redirect 视参数

最后三条原则 :不确定永久与否,选临时(302);目标来自用户输入,永远校验为本地;需要把 POST 原样带过去,才用 307/308。

相关推荐
掘金者阿豪1 小时前
Node.js 连金仓数据库(下篇):连接池、事务和那些坑
后端
郑州光合科技余经理1 小时前
海外版外卖系统源码:支付/地图/多语言核心代码实现
android·java·前端·后端·架构·uni-app·php
jeffer_liu2 小时前
Spring AI 生产级实战:多模态
java·人工智能·后端·spring·大模型
Gopher_HBo2 小时前
Go语言学习笔记(五)异常处理
后端
SimonKing2 小时前
你还在靠重启来调线程池?别人已经做到了实时调控,3分钟接入
java·后端·程序员
IT_陈寒2 小时前
Redis客户端连接池不关闭的后果,程序直接崩给我看
前端·人工智能·后端
可可嘻嘻大老虎3 小时前
SpringBoot拦截器防重复提交实战
java·spring boot·后端
RainCityLucky3 小时前
Java Swing 自定义组件库分享(十一)
java·笔记·后端
cheems95273 小时前
[开发日记]Spring Boot + MyBatis-Plus 抽奖系统排障实录:从 JWT 被拦截到雪花 ID 失控,我是怎样一步步修通登录与人员列表的
spring boot·后端·mybatis