转发 & 重定向

1,286 阅读12分钟

重定向 (redirect)

重定向是指当浏览器请求一个URL时,服务器返回一个重定向指令,告诉浏览器地址已经变了,麻烦使用新的URL再重新发送新请求。

重定向机制的原理是将接收响应的url 地址设置为 window.location ,并立即加载新的页面。

重定向的响应状态码为3xx。

在HTTP协议中,服务器通过发送特定的响应实现重定向,浏览器在接收到响应后,可根据状态码判定重定向,并使用指定的新URL重新请求,重定向操作对于用户来说是不可见的。

重定向过程: 当浏览器收到 302 响应后,从响应头中的 Location 获取新的URL,发起一个新的请求。

重定向类型

  • 永久重定向:301,308

是指原URL不再被使用,应优先选择新的URL,搜索引擎机器人会在遇到该状态码时,触发更新操作,使用新的URL

  • 临时重定向:302,303, 307

如果请求的资源临时不可用,但可从其他地方访问,于是就通过响应 Header 的 Location 字段将这个临时地址告知给用户。。

  • 特殊重定向:304、300

304 Not Modified 资源未被修改,会从本地缓存中获取网页;

300 手工重定向,用户可选择重定向的页面。

请求302: 在收到 302 响应之后,浏览器会发起新的请求,

先来看一下什么是Location

Location 首部指定的是需要将页面重新定向至的地址。一般在响应码为 3xx 的响应中才会有意义。

发送新请求,获取 Location 指向的新页面所采用的方法与初始请求使用的方法以及重定向的类型相关:

  • 303 (See Also) 始终引致请求使用 GET 方法,而 307 (Temporary Redirect) 和 308 (Permanent Redirect) 则不转变初始请求中的所使用的方法;
  • 301 (Permanent Redirect) 和 302 (Found) 在大多数情况下不会转变初始请求中的方法。

状态码为上述之一的所有响应都会带有一个 Location 首部。

fetch 中的重定向

在 fetch 请求中无法捕获重定向时的状态码,且无法捕获接口返回的信息,直接重定向;

当重定向的页面发生跨域时 Location 对应的服务器不接受跨域请求

  • 如果不是同源的,我们就不能访问窗口中的内容:变量,文档,任何东西。
  • 唯一例外是location:我们可以修改它,使用它进行重定向。但是无法读取 location 。因此,我们无法看到用户当前所处的位置,也就不会泄露任何信息。

fetch 请求流程

fetch 发送请求 -->

服务器返回 response 并且带有状态码(比如200) -->

浏览器接收到响应,结果递交给fetch -->

我们从fetch的回调函数获取相应数据 ✅

fetch 请求 重定向流程

fetch 发送请求 -->

服务器返回 response(带有location) 并且带有状态码302 -->

浏览器接收到响应,通过location进行跳转 -->

服务器返回 response 并且带有状态码(比如200) -->

fetch 302 浏览器会帮助重定向到新的接口,并返回最终的数据,发生了两次请求

当浏览器检查到 headers 中存在 Location,会直接进行跳转,不会告知任何请求发送者(fetch),这时候发送者会以为请求还在处理中。所以此时的 fetch 的 then 和catch 都捕获不到信息

所以在重 定向时无法直接捕获状态码。


如何解决

  1. 配置 fetch 的 redirect

fetch 的 options 配置项 redirect,用于配置可用的 redirect 模式。

redirect 的值有:

  • follow:默认, 自动重定向;
  • error:如果产生重定向将自动终止并且抛出一个错误;
  • manual:手动处理重定向;

error

如果产生重定向将自动终止并且抛出一个错误。此错误可以在 fetch catch 回调函数中捕获:TypeError: Failed to fetch。

fetch 只有服务器错误才调用 catch,其他都会调用 then 函数,那么 302 为什么会调用catch?

不是请求 302 导致 catch 被调用, 而是重定向后的请求返回的 response 导致 catch

manual

手动处理重定向。通过这种方法只能知道发生了重定向,但是 response 的内容非常有限,无法获取到具体的信息。

浏览器的重定向

浏览器是否会改变地址栏并跳转, 其实取决于请求的 URL 是否会让文档加载/重新加载

这里所指的文档加载/重新加载,既包括当我们通过浏览器地址栏访问某个 URL、刷新页面所导致的文档加载,也包括我们通过 window.location 让文档用新的 URL 加载。

如果我们请求的 URL 返回了 3XX 响应,浏览器才会读取响应 Header 里面的 Location 字段,并将浏览器的地址栏修改为这个字段所指定的 URL,然后跳转到这个 URL。

文档的加载/ 重载 都会产生一个 Document 类型请求

但是在 fetch 请求得到的(3XX)重定向,浏览器是不会改变当前地址栏的url,也不会触发页面的跳转

因为 fetch 请求是在后台进行的,它只是获取了重定向后的响应,但并没有对页面进行跳转。

相反,它会返回一个重定向响应,这个响应包含一个 redirected 属性,用于指示是否发生了重定向。

  • 如果 redirected 属性为 true,则表示发生了重定向,但是并不会自动跳转到重定向后的页面,需要在代码中手动处理这个响应。
  • 如果 redirected 属性为 false,则表示没有发生重定向。

与此不同的是,当用户在浏览器中直接访问某个 URL 时,浏览器会根据服务器返回的响应头信息对页面进行跳转,并更新地址栏的 URL。

重定向的实现

设置重定向方法

可以通过HTML的metay元素,或者JS实现重定向

<head>
  <meta http-equiv="Refresh" content="0; URL=http://example.com/" />
</head>

content属性值,第一个数字表示等待多少秒后进行跳转。

表单提交

通过原生的表单提交实现接口的重定向:

表单通过在 HTML 上设置 action 和 method 来提交。在表单提交之后,浏览器会自动跳转到 action 所对应的地址,并使用 method 所指定的方法向服务器发起请求。

当用户提交表单时,浏览器会将表单数据发送到服务器,服务器收到请求后会进行重定向操作,并将浏览器重定向跳转到表单中设置的 action 属性指定的页面。这个过程也被称为表单提交后的重定向。

<template>
  <form v-if="clientInfo" ref="form" name="consent_form" method="post" action="/auth/oauth2/authorize">
    <input type="hidden" name="client_id" :value="clientInfo.clientId">
    <input type="hidden" name="state" :value="clientInfo.state">
    <input type="hidden" name="scope" :value="clientInfo.scope">
  </form>
</template>

<script setup lang="ts">
const clientInfo = ref()

const handleAgree = async () => {
  if (checked.value) {
    // submit()  提交表单
    form.value.submit()
  } else {
    showToast({
      message: '请确认并勾选授权第三方应用',
    })
  }
}

</script>

在跳转到新页面之前,浏览器会将表单数据封装成一个 POST 请求,并发送给服务器。服务器收到请求后,可以对表单数据进行处理,并返回一个新的页面给浏览器,浏览器会自动跳转到这个新页面。

需要注意的是,如果表单中设置的 action 属性是一个相对路径,则浏览器会将其解析为相对于当前页面的路径。

iframe 跨窗口通信

在两个不同的窗口之间进行跨域通信,可以使用 iframe 和 postMessage API 实现。

iframe 标签承载了一个单独的嵌入的窗口,它有自己的 documentwindow

当我们访问嵌入的窗口中的东西时,浏览器会检查 iframe 是否具有相同的源。如果不是,则会拒绝访问(对 location 进行写入是一个例外,它是会被允许的)。

  • iframe.contentWindow 来获取 iframe中的 window
  • iframe.contentDocument 来获取 iframe中的 document (
    • iframe.contentDocument 是 iframe.contentWindow.document 的简写)
  • window.frames —— “子”窗口的集合(用于嵌套的 iframe)。
  • window.parent —— 对“父”(外部)窗口的引用。
  • window.top —— 对最顶级父窗口的引用。

具体做法如下

  1. 在一个窗口中创建一个 iframe 元素,并设置其 src 属性为另一个窗口的 URL。
  2. 在 iframe 中的文档中,通过 postMessage API 将消息发送到另一个窗口。
  3. 在另一个窗口中,通过监听 message 事件,接收到来自 iframe 窗口的消息,并进行处理。

top 属性来检查当前的文档是否是在 iframe 内打开的:

window.frames[0].parent === window; // true

if (window === window.top) { 
   alert('不是在 iframe 中打开的');
 } else {
   alert('在 iframe 中打开的');
 }
window.postMessage

postMessage 接口允许窗口之间相互通信,无论它们来自什么源。

因此,这是解决“同源”策略的方式之一。

window.postMessage方法可以安全地实现跨源通信,写明目标窗口的协议、主机地址或端口就可以发信息给它。

postMessage

想要发送消息的窗口需要调用接收窗口的 postMessage 方法

如果我们想把消息发送给 win,我们应该调用 win.postMessage(data, targetOrigin)。

onMessage

为了接收消息,目标窗口应该在 message 事件上有一个处理程序。当 postMessage 被调用时触发该事件(并且 targetOrigin 检查成功)。

addEventListener 监听是否有消息传来

 window.addEventListener("message", function(event) {
   console.log(event)
   if (event.origin != 'http://http://127.0.0.1:8080') {
     // 来自未知的源的内容,我们忽略它
     return;
   }
 
   if (window == event.source) {
     // chrome 下, 页面初次加载后会触发一次 message 事件, event.source 是 window 对象
     // 此时 event.source.postMessage 会形成死循环
     // 因此,要跳过第一次的初始化触发的情况
     return
   }
     
   console.log( "received: " + event.data );
 
   // 可以使用 event.source.postMessage(...) 向回发送消息
   event.source.postMessage('i am 2.html')
 }, source);
iframe的应用

通过 iframe 的跨窗口通信让父级实现重定向 具体做法如下

  1. 在一个窗口中创建一个 iframe 元素,并设置其 src 属性为另一个窗口的 URL。
  2. 在 iframe 中的文档中,通过 postMessage API 将消息发送到另一个窗口。
  3. 在另一个窗口中,通过监听 message 事件,接收到来自 iframe 窗口的消息,并进行处理。

在父窗口中:

<iframe id="myFrame" src="http://example.com"></iframe>
<script>
  var frame = document.getElementById('myFrame');
  frame.contentWindow.postMessage('Hello from parent', 'http://example.com');
</script>

在子窗口中:

window.addEventListener('message', function(event) {
  if (event.origin === 'http://example.com') {
    console.log('Received message: ' + event.data);
    event.source.postMessage('Hello from child', event.origin);
  }
});


在上面的例子中,父窗口向子窗口发送了一条消息,子窗口收到消息后又向父窗口发送了一条回复消息。需要注意的是,postMessage API 中的第二个参数是目标窗口的 origin,用于限制消息只能被发送到指定的窗口。


同域才能访问父/祖父/组祖父/... 页面的方法

跨域也能获取并覆盖 父/祖父/组祖父/... 页面的 cookie

如果cookie 的 domain不一致则不能互相访问、修改(设置domain解决跨域的原理)

设置domain

document.domain作用是获取/设置当前文档的原始域部分,同源策略会判断两个文档的原始域是否相同来判断是否跨域。

document.domain = 'demo.com'

window.location + 3XX 响应

window.location 能够让文档用新的 URL 加载,默认会将新的 URL push 到浏览器 history 的路由堆栈里面,当接口重定向时可以用 window.location ,将页面跳转到重定向页面

应用场景:

在使用 OAuth2 鉴权时(比如微信登录),可以通过 window.location 重定向到自己服务器的授权地址(支持多个平台登录时,可以由后端统一处理),然后服务器会生成一个三方授权点的地址,并通过 302 响应告知给浏览器,浏览器在收到响应之后会跳转到这个三方授权点的 URL(微信登录页),完成授权之后,三方授权页面会通过 window.location 再重定向回我们自己的页面。

通过 window.location 再配合 302 响应,我们可以快速将用户导向三方授权点。

Nginx重定向

rewrite

nginx的rewrite主要功能就是实现URL的重定向,其语法规则如下:

rewrite <regex> <replacement> [flag]

regex 正则匹配需要重定向的url replacement 替换内容,

将正则匹配的内容替换成 replacement flag 标记,具体如下:

  • last:本条规则匹配之后,继续向下匹配新的rewrite;
  • break:本条规则匹配完成即终止,后面的规则不再匹配;
  • redirect:返回302临时重定向;
  • permanent:返回301永久重定向;

rewirte参数的标签段位置:server,location,if

网站的代理

// 将 mi.com 重定向 www.mi.com
server {
  listen 80;
  server_name mi.com;
  rewrite ^/(.*) http://www.mi.com/$1 permanent;
}

接口代理

// vue3
server: {
  proxy: {
    '/auth': {
      target: env.VITE_API_URL,
      rewrite: path => path.replace(//auth/, ''),
    },
  },
  host: '0.0.0.0',
},

// vue2
devServer: {
    disableHostCheck: true,
    proxy: {
      '/api': {
        target: process.env.VUE_APP_API_ROOT,
        pathRewrite: { '^/api': '' },
        secure: false,
        changeOrigin: true
      }
    }
  }

转发 (forward)

转发是在Web服务器内部完成的

服务器接收到客户端的请求后,将请求转发给另一个资源进行处理,并将处理结果返回给客户端。

在转发的过程中,客户端只发起了一次请求,而服务器则负责将请求转发给其他资源进行处理,并将处理结果返回给客户端。因此,与重定向不同,转发不会改变客户端的 URL。

ForwardServlet 在收到请求后,它并不自己发送响应,而是把请求和响应都转发给路径为 /hello 的 Servlet

后续请求的处理实际上是由HelloServlet完成的

使用转发的时候,浏览器的地址栏路径仍然是/morning,浏览器并不知道该请求在Web服务器内部实际上做了一次转发。

转发 和 重定向的区别

转发:

  • 在服务端完成
  • 浏览器不知道服务器内部的转发逻辑
  • 浏览器地址栏中的地址不会改变,是一次请求

重定向:

  • 由浏览器完成
  • 浏览器知道重定向规则,并且会自动发起新的HTTP请求,跳转到新的URL
  • 且浏览器地址栏会变化,是二次请求
    • 第一次是原始请求,第二次是重定向请求。

参考

  1. Http 3XX 重定向
  2. MDN HTTP的重定向
  3. iframe 跨窗口通信