在 Nextjs/App Router 中,如何使用服务器操作将 fetch 请求中的 httponly cookie 传递给 API

问题描述 投票:0回答:1

我有一个使用 App Router 设置的 Nextjs 13 前端和一个使用 Doorkeeper 进行身份验证的仅 Rails 7 API 后端。

我有一个新的产品表单,它利用 Nextjs“服务器操作”将 POST 请求发送到 Rails 后端的

products#create
路由。我想使用服务器操作,以便在创建产品后我可以调用 nextjs
revalidatePath()
来清除缓存并使用更新的产品列表刷新 UI。

但是,由于我使用配置为通过 HttpOnly cookie 进行身份验证的 Doorkeeper,并且

products#create
方法是受保护的路由,因此我似乎不知道如何传递所需的
access_token
cookie。

似乎是:

  1. 我使用客户端获取调用,并让浏览器自动将身份验证 cookie 包含在请求中,但 nextjs 缓存不会清除或更新新产品信息,

或,

  1. 我使用可以调用 revalidatePath() 的服务器操作,但我需要重构我的 Doorkeeper 配置,使其不基于 HttpOnly cookie,这是我想要维护的安全措施。

我试图找到一个 nextjs 中间件解决方案,但即使我可以在控制台记录

request.headers
时看到包含 access_token 并且它似乎位于 devTools 中的网络请求中,但 cookie 没有到达后端的 Doorkeeper。当我从服务器操作中删除 revalidatePath() 并删除“使用服务器”标志时,cookie 确实会通过(因此它不再是服务器操作)。

这是第 22 条军规场景吗?还是我在 Nextjs 文档中遗漏了某些内容?我猜测 Nextjs 服务器无法访问浏览器中的 HttpOnly cookie,因此在使用服务器操作时可能无法将它们包含在服务器端。但这让我很困惑为什么 cookie 会出现在 devTools 的网络选项卡中。

我希望有人可能使用过类似的设置,并找到了我可以在这种情况下实施的最佳实践或解决方法。

非常感谢!

这是向

products#create
发送获取请求的服务器操作:

// actions.ts
'use server'
export const createProduct = async (data: FormData) => {
  const url = `${ baseApiUrl() }/v1/products`;

  const response = await fetch(url, {
    credentials: 'include',
    method: 'POST',
    headers: {
      'Authorization': `Basic ${ doorkeeperCredentials() }`,
    },
    body: configureData(data)
  });

  revalidatePath('/products');

  return response.json();
};

这是

ProductForm
组件:

// product-form.component.tsx
'use client'

// library
import { useState, useEffect } from "react";

// api
import { createProduct } from "../../api/products-api";
import { Category } from "@/app/categories/page";
import { getAllCategories } from "@/app/api/categories-api";

const ProductForm = () => {
  // state
  const [ loading, setLoading ] = useState(true);
  const [ categories, setCategories ] = useState<Category[] | null>(null)

  useEffect(() => {
    const getCategories = async () => {
      const response = await getAllCategories();
      const categories: Category[] = await response.json();
      setCategories(categories);
    };

    categories ? setLoading(false) : getCategories();
  }, [ categories ])

  if (loading) return <p>Loading...</p>;

  return (
    <form 
      id="product"
      className="product-form"
      action={ formData => createProduct(formData) }
    >
      {/* product name */}
      <label 
        className="product-form__label"
        htmlFor="name"
      >
        Name
      </label>
      <input
        id="name" 
        className="product-form__input"
        type="text"
        autoComplete="false"
        name="product[name]"
      />

      {/* product description */}
      <label
        className="product-form__label" 
        htmlFor="short-description"
      >
        Description
      </label>
      <textarea
        id="short-description"
        className="product-form__textarea" 
        name="product[short_description]"
      />

      {/* product images */}
      <label 
        className="product-form__label"
        htmlFor="product-images"
      >
        Images
      </label>
      <input 
        id="product-images"
        className="product-form__attach-button"
        type="file"
        name="product[product_images][]"
        multiple
      />

      <div className="category-select">
        { categories && categories.map((category) => 
          <div key={ category.id }>
            <input 
              id={ `category-${ category.name }` }
              type='checkbox'
              value={ category.id }
              name={ `product[category_ids][]`}
            />
            <label htmlFor={  `category-${ category.name }` }>
              { category.name }
            </label>
          </div>
        )}
      </div>

      {/* submit button */}
      <button 
        className="product-form__button"
        type='submit'
      >
        Submit
      </button>
    </form>
  )
};

export default ProductForm;

这是我的下一个配置:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true
  },
  images: {
    domains: [
      'localhost'
    ]
  }
}

module.exports = nextConfig
reactjs next.js cookies rails-api doorkeeper
1个回答
0
投票

我找到了解决此问题的解决方法。感觉有点笨重,所以如果有更干净的解决方案或更好的实践,我很想知道。

在单独的

server-actions.ts
文件中,我导出了一个仅调用
revalidatePath()
的异步函数。这显然感觉多余,但它遵循 Nextjs 协议在客户端组件内使用服务器操作:要么从顶部带有“use server”的文件导出操作,要么直接传递带有“use server”的异步函数它从服务器组件到客户端组件。

由于我想在一些地方重用此功能,所以我选择从单独的文件导出。

我在 ProductForm 组件中以相同的方式调用我的

createProduct(formData)
函数:

    <form 
      id="product"
      className="product-form"
      action={ formData => createProduct(formData) }
    >
      {/* product name */}
      <label 
        className="product-form__label"
        htmlFor="name"
      >
        Name
      </label>
      <input
        id="name" 
        className="product-form__input"
        type="text"
        autoComplete="false"
        name="product[name]"
      />

    ...

      {/* submit button */}
      <button 
        className="product-form__button"
        type='submit'
      >
        Submit
      </button>
    </form>

然后在我的

products-api.ts
文件中导入我在
revalidate
中定义的
server-actions.ts
函数,并在 createProduct() 函数中调用它。这样,createProduct 仍然作为客户端函数运行,将 HttpOnly cookie 传递给 Rails 后端的 Doorkeeper,然后在成功创建产品后强制进行服务器端重新验证。

export const createProduct = async (data: FormData) => {
  const url = `${ baseApiUrl() }/v1/products`;

  const response = await fetch(url, {
    credentials: 'include',
    method: 'POST',
    headers: {
      'Authorization': `Basic ${ doorkeeperCredentials() }`,
    },
    body: configureData(data)
  });

  revalidate('/');

  return response.json();
};

我不确定像这样组合客户端和服务器操作是否遵循 Nextjs 最佳实践,但根据我的需求,我现在一切都按我希望的那样工作。

© www.soinside.com 2019 - 2024. All rights reserved.