重定向是 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);
参数语义与上一节的表格完全一一对应:permanent 为 true 时是 301 或 308,preserveMethod 为 true 时是 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,适合路由结构和控制器结构解耦的场景。
每一类都有完整的四方法矩阵(基础版、Permanent、PreserveMethod、PermanentPreserveMethod),命名规律完全一致,不再赘述。
3.2 一个常被忽视的细节:IKeepTempDataResult
RedirectResult、RedirectToActionResult、RedirectToRouteResult 都实现了 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 来描述响应,重定向由静态类 Results 与 TypedResults 提供。
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)、LocalRedirectPermanentPreserveMethod(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 被认定为"本地",需满足以下条件:
- 不包含 host(主机)或 authority(授权)部分 ------也就是说不能是
https://evil.com/...这种带域名的绝对 URL。 - 拥有一条绝对路径 (以
/开头)。 - 使用虚拟路径语法
~/的 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。