转发源请求的元数据

前面的章节说道,在 SSR 场景中,Next.js 渲染服务器向 API 服务器请求数据时,其实是反向代理了真实的用户请求。

Excalidraw Loading...

如上图所示,通过渲染服务器发送的请求并不会转发真实用户请求的头部信息和 IP 等其他信息。而如果不转发这些信息,那么对于数据分析或者需要使用 IP 或者 ua 的场景就会非常不友好,例如 rate limit 我们一般使用 ua 或者 IP 作为标识符。

所以这节我们对这些请求做特殊处理。

在 Page Router 下

在传统的路由模式中,渲染服务器发送的请求一般在 getInitialProps 或者 getServerSideProps 方法内部。

下面的例子用我们使用 axios 作为请求库,当然你可以选择其他。

我们先以 getServerSideProps 为例。

getServerSideProps 接受一个 ctx 对象,其中包含 req: ImcomingMessage 对象。从上面我们可以获取到原始请求的 headers 和 ip 信息。

import type { GetServerSideProps } from 'next'

export default function Home() {
  return <div />
}

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const { req } = ctx

  req.headers
  req.connection

  return {
    props: {},
  }
}

那么在渲染服务器发送请求之前,我们需要对把元数据注入到请求实例中。

我们可以封装一个方法实现:

export const attachRequestProxy = (request?: IncomingMessage) => {
  if (!request) {
    return
  }

  let ip =
    ((request.headers['x-forwarded-for'] ||
      request.headers['x-real-ip'] ||
      request.connection.remoteAddress ||
      request.socket.remoteAddress) as string) || undefined
  if (ip && ip.split(',').length > 0) {
    ip = ip.split(',')[0]
  }
  ip && ($axios.defaults.headers.common['x-forwarded-for'] = ip as string)

  $axios.defaults.headers.common['User-Agent'] =
    `${request.headers['user-agent']} NextJS/v${PKG.dependencies.next}`

  // forward auth token
  const cookie = request.headers.cookie
  if (cookie) {
    const token = cookie
      .split(';')
      .find((str) => {
        const [key] = str.split('=')

        return key === TokenKey
      })
      ?.split('=')[1]
    if (token) {
      $axios.defaults.headers['Authorization'] = `bearer ${token.replace(
        /^Bearer\s/i,
        '',
      )}`
    }
  }
}

上面的方法中可以把原始请求中的 user-agent 附加到请求示例中,提取真实 ip 附加到 headers 的 x-forwarded-for 上。随后还可以针对 token 等其他信息的转发。

然后这样使用。

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const { req } = ctx
  attachRequestProxy(ctx.req)

  await $axios.get('http://localhost:9999/api/test') // do your any request via axios
  return {
    props: {},
  }
}

getInitialProps 中使用方式也大同小异。

但是由于 getInitialProps 会在 Client 和 Server 端混合执行,所以需要对 attachRequestProxy 方法加一点判断。

export const attachRequestProxy = (request?: IncomingMessage) => {
  if (!request) { 
    return
  } 

  if (!isServerSide()) { 
    return
  } 

  let ip =
    ((request.headers['x-forwarded-for'] ||
      request.headers['X-Forwarded-For'] ||
      request.headers['X-Real-IP'] ||
      request.headers['x-real-ip'] ||
      request.connection.remoteAddress ||
      request.socket.remoteAddress) as string) || undefined
  if (ip && ip.split(',').length > 0) {
    ip = ip.split(',')[0]
  }
  ip && ($axios.defaults.headers.common['x-forwarded-for'] = ip as string)

  $axios.defaults.headers.common['User-Agent'] =
    `${request.headers['user-agent']} NextJS/v${PKG.dependencies.next} Kami/${version}`

  // forward auth token
  const cookie = request.headers.cookie
  if (cookie) {
    const token = cookie
      .split(';')
      .find((str) => {
        const [key] = str.split('=')

        return key === TokenKey
      })
      ?.split('=')[1]
    if (token) {
      $axios.defaults.headers['Authorization'] = `bearer ${token.replace(
        /^Bearer\s/i,
        '',
      )}`
    }
  }
}

然后这样使用。

import type { NextPage } from 'next'

import { $axios, attachRequestProxy } from '~/lib/axios'

const Home: NextPage = () => {
  return <div />
}
export default Home

Home.getInitialProps = async (ctx) => {
  attachRequestProxy(ctx.req)

  await $axios.get('http://localhost:9999/api/test')
  return {}
}

我们来看看效果。写一个接口 /api/test 如下:

api/test.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>,
) {
  console.log(
    'req.headers.user-agent',
    req.headers['user-agent'],

    '  req.connection.remoteAddress',
    req.connection.remoteAddress,
    '  req.socket.remoteAddress',
    req.socket.remoteAddress,
    '  req.connection.remoteAddress',
    req.connection.remoteAddress,
    '  req.socket.remoteAddress',
    req.socket.remoteAddress,
    "  req.headers['x-forwarded-for']",
    req.headers['x-forwarded-for'],
  )
  res.status(200).json({})
}

本地启动 dev server,然后我们使用内网地址访问 http://10.0.0.89:9999 (这是我的环境,按需修改。)

随后得到 server 的控制台输出:

req.headers.user-agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 NextJS/v14.1.1
req.connection.remoteAddress ::1
req.socket.remoteAddress ::1 
req.connection.remoteAddress ::1   
req.socket.remoteAddress ::1   
req.headers['x-forwarded-for'] ::ffff:10.0.0.89

可以看到 user-agentx-forwarded-for 都是源请求的数据。

上面的例子位于:https://github.com/innei-dev/nextjs-book/tree/main/demo/request-proxy-pages

在 App Router 下

在 App Router 下,即使在 Server Component 中我们也是无法获取到 req: ImcomingMessage 的,原因是在这个模式下,组件和组件是可被拆分渲染的,所以我们无法在任何地方获取 req,除非在 middleware 中。但是 middleware 中我们不会做任何 UI render 和数据请求。

这里我们可以使用 headers() 方法获取 reqheaders,仅限于在 Server Component 中取到。

import 'server-only'
import { headers } from 'next/headers'

export const attachUAAndRealIp = () => {
  const { get } = headers()

  const ua = get('user-agent')
  const ip =
    get('x-real-ip') ||
    get('x-forwarded-for') ||
    get('remote-addr') ||
    get('cf-connecting-ip')
  $axios.defaults.headers.common['X-Real-IP'] = ip
  $axios.defaults.headers.common['X-Forwarded-For'] = ip
  $axios.defaults.headers.common[
    'User-Agent'
  ] = `${ua} NextJS/v${PKG.dependencies.next} ${PKG.name}/${PKG.version}`
}
Note

我们可以安装 server-only 来确保只有在 Server Side 引用该文件。

app/pages.tsx
import { attachUAAndRealIp } from '~/lib/attach-req'
import { $axios } from '~/lib/axios'

export default async function Home() {
  attachUAAndRealIp()
  await $axios.get('http://localhost:9999/api/test')
  return <div />
}

编写 App router 下的测试 api。

app/api/test/route.ts
import type { NextRequest } from 'next/server'

export const GET = (req: NextRequest) => {
  console.log(
    'req.headers.user-agent',
    req.headers.get('user-agent'),

    '  req.ip',
    req.ip,
    "  req.headers['x-forwarded-for']",
    req.headers.get('x-forwarded-for'),
  )
  return new Response()
}

访问 内网地址,输出:

req.headers.user-agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 NextJS/v14.1.1 request-proxy/0.1.0
req.ip undefined
req.headers['x-forwarded-for'] ::ffff:10.0.0.89

可以看到 x-forwarded-for 正确的附加了源请求的 IP,并且 user-agent 也是源请求的。


最后更新于 2024/5/22 21:03:39

更新历史

本书还在编写中..

前往 https://innei.in/posts/tech/my-first-nextjs-book-here#comment 发表你的观点吧。