繁体   English   中英

NEXTJS:无效的钩子调用

[英]NEXTJS: Invalid hook call

我在 nextjs 12 中使用新的中间件,并尝试使用 firebase 添加身份验证。

但是当我尝试使用钩子useAuthState时,它给我一个错误消息:“无效的钩子调用。钩子只能在 function 组件的主体内部调用。这可能由于以下原因之一而发生:

  1. 您可能有不匹配的 React 版本和渲染器(例如 React DOM)
  2. 你可能违反了 Hooks 规则
  3. 您可能在同一个应用程序中有多个 React 副本 请参阅https://reactjs.org/link/invalid-hook-call以获取有关如何调试和修复此问题的提示。 无效的”

除了在 pages 目录中创建 2 个组件外,我没有对应用程序进行任何更改

login.js 和 _middleware.js

这是我的 _middleware.js

import { NextResponse } from "next/server";
// import firebase from "firebase"
import { initializeApp } from "firebase/app";
import "firebase/auth";
import { getAuth } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";

initializeApp({ ... });

const auth = getAuth();

export async function middleware(req) {
  const [user] = useAuthState(auth);
  const { pathname } = req.nextUrl;

  if (!user) {
    return NextResponse.redirect("/login");
  }

  return NextResponse.next();
}

这是我的 login.js

function login() {
  const signInWithGoogle = () => {
    console.log("Clicked!");
  };

  return (
    <div>
      <button onClick={signInWithGoogle}>Sign in with google</button>
    </div>
  );
}

export default login;

这是我的 package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "firebase": "^9.5.0",
    "next": "^12.0.4",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-firebase-hooks": "^4.0.1"
  },
  "devDependencies": {
    "autoprefixer": "^10.2.6",
    "postcss": "^8.3.5",
    "tailwindcss": "^2.2.4"
  }
}

根据反应文档

不要从常规的 JavaScript 函数调用 Hooks。 相反,您可以:

✅ 从 React function 组件调用 Hooks。
✅ 从自定义 Hooks 调用 Hooks(我们将在下一页了解它们)。

所以你在下面的常规 Javascript function 中使用钩子

export async function middleware(req) { // this is regular JavaScript function
  const [user] = useAuthState(auth); // here you're using hook
  const { pathname } = req.nextUrl;

  if (!user) {
    return NextResponse.redirect("/login");
  }

  return NextResponse.next();
}

您的情况的解决方案可能是

import React, { useEffect } from "react";
import { NextResponse } from "next/server";
import { useAuthState } from "react-firebase-hooks/auth";

function login() {
  const [user] = useAuthState(auth); // here you're using hook
  const { pathname } = req.nextUrl;
  
  useEffect(() => {
    if (!user) {
      return NextResponse.redirect("/login");
    }
  
    return NextResponse.next();
  }, [user]);

  const signInWithGoogle = () => {
    console.log("Clicked!");
  };


  return (
    <div>
      <button onClick={signInWithGoogle}>Sign in with google</button>
    </div>
  );
}

export default login;

不要忘记在您的组件中导入所需的导入

在 nextjs文档之后:

下一个/服务器

next/server 模块为仅服务器助手提供了几个导出,例如Middleware

中间件

中间件使您能够使用代码而不是配置。 这为您在 Next.js 中提供了充分的灵活性,因为您可以在请求完成之前运行代码。 根据用户的传入请求,您可以通过重写、重定向、添加标头甚至流式传输 HTML 来修改响应。

中间件不是反应组件,不能使用钩子。

可悲的是中间件正在使用 V8 引擎https://v8.dev/和 Firebase package 不支持这个(浪费了太多时间)。 要对用户进行身份验证,您应该使用像jose https://www.npmjs.com/package/jose这样的库自行解密

我设法使用 Node 18 和 node-fetch 来处理它。 您的代码在 typescript 上应如下所示:

import { NextResponse } from 'next/server'
import { jwtVerify, importX509, JWTPayload } from 'jose'
import fetch from 'node-fetch'

import type { NextRequest } from 'next/server'

interface TokenHeader {
  alg: string
  kid: string
  typ: string
}

const authAlgorithm = 'RS256'

const appID = 'your-app-id-here'
const tokenISS = `https://securetoken.google.com/${appID}`

function verifyFirebasePayload(payload: JWTPayload) {
  const currentDate = new Date()
  if (
    !payload ||
    (payload.exp ?? 0) * 1000 < currentDate.getTime() ||
    (payload.iat ?? currentDate.getTime()) * 1000 > currentDate.getTime() ||
    payload.aud !== appID ||
    payload.iss !== tokenISS ||
    !payload.sub ||
    !payload.user_id ||
    payload.sub !== payload.user_id ||
    (payload.auth_time as number) * 1000 > currentDate.getTime()
  ) {
    throw Error('Token expired')
  }
}

// function used to removed token cookies if it's invalid
function responseWithoutCookies(request: NextRequest) {
  const response = NextResponse.redirect(new URL('/', request.url))
  const { value, options } = request.cookies.getWithOptions('token')
  if (value) {
    response.cookies.set('token', value, options)
    response.cookies.delete('token')
  }
  return response
}

async function getPayloadFromToken(token: string) {
  const currentKeysx509: Record<string, string> = await (
    await fetch(
      'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
    )
  ).json() // this may need to be improved, too many queries?
  const headerbase64 = token.split('.')[0] ?? ''
  const headerConverted = JSON.parse(Buffer.from(headerbase64, 'base64').toString()) as TokenHeader

  const matchKeyx509 = currentKeysx509[headerConverted.kid]
  if (!matchKeyx509) {
    throw Error('No match key')
  }
  const publicKey = await importX509(matchKeyx509, authAlgorithm)

  const { payload } = await jwtVerify(token, publicKey, {
    issuer: tokenISS,
    audience: appID,
    algorithms: [authAlgorithm]
  })
  return payload
}

async function middleware(request: NextRequest) {
  const isHomepage = request.nextUrl.pathname === '/'
  if (isHomepage || request.nextUrl.pathname.startsWith('/_next')) {
    return NextResponse.next()
  } // not necessary to process when the middleware is used internally
  const token = request.cookies.get('token')

  let isAuthenticated = false

  if (!isHomepage && token) {
    if (process.env.NEXT_PUBLIC_ENV !== 'prod') { // just for testing on `dev`
      return NextResponse.next()
    }
    try {
      const payload = await getPayloadFromToken(token)
      verifyFirebasePayload(payload)

      isAuthenticated = true
    } catch (error) {
      isAuthenticated = false
    }
  }

  if (isAuthenticated) {
    return NextResponse.next()
  }

  return responseWithoutCookies(request)
}

export default middleware

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM