l

lynn1286

V1

2022/04/05阅读:75主题:默认主题

next12 入门与实践【实现登录鉴权】

文末有代码地址 😄
如果文章有帮助到你,请给我点个赞 👍

由于篇幅问题,本文只介绍我在看 Next 文档的时候感觉不太理解或者是项目中必须使用到的 api,例如 导航

环境变量

Next.js允许你在 .env (所有环境下)、**.env.development** (development 环境)和 **.env.production **(production 环境) 中设置默认值,另外还有个不常见的 **.env.test **(test 环境), 一般用于 测试用例

注意:由于 .env、.env.development 和 .env.production 文件定义了默认设置,因此应该提交到源码仓库中。而 .env.*.local 应当添加到 .gitignore 中,因为这类文件是需要被忽略的。.env.local 是被用来存储密钥类信息的。

在 node 环境下 访问环境变量 可以通过 **process.env.xxx **来访问 环境变量。

默认情况下,环境变量仅在 Node.js 环境中可用,这意味着它们不会暴露到浏览器端。
为了向浏览器暴露环境变量,你必须在变量前添加 **NEXT_PUBLIC_ **前缀。

例如:

NEXT_PUBLIC_ANALYTICS_ID=localhost

在 Typescript 中, 想要有代码提示,你需要在 type 文件夹中声明env类型,才能够拥有代码提示。

创建一个声明文件 process.d.ts

declare namespace NodeJS {
  export interface ProcessEnv {
    NEXTAUTH_URL: string
    NEXTAUTH_SECRET: string
    GITHUB_ID: string
    GITHUB_SECRET: string
    FACEBOOK_ID: string
    FACEBOOK_SECRET: string
    TWITTER_ID: string
    TWITTER_SECRET: string
    GOOGLE_ID: string
    GOOGLE_SECRET: string
    AUTH0_ID: string
    AUTH0_SECRET: string
    ICON_FONT_URL: string
  }
}

接着修改 **tsconfig.json **中的 include 字段

{
 "include": [
  "types/process.d.ts"// 
  "next-env.d.ts",
  "types/next-auth.d.ts",
  "**/*.ts",
  "**/*.tsx"
 ],
}

路由

Next 路由是自动生成的,我们只需要在 pages 目录下新建文件 即可通过文件名访问我们的页面,例如:

// pages/lynn.ts
function Lynn(){
    return (<button>lynn</button>)
}

export default Lynn;

启动项目后是运行在 3000 端口下的, 我们加上 /lynn 路径即可访问到我们这个页面。

标签式导航

import Link from 'next/link'

export default ()=>(
 <>
  <Link href="/"><a>返回首页</a></Link>
 </>
)

编程式导航

import { useRouter } from 'next/router'

export default ()=>(
  const router = useRouter()
  
  const handleClick = (e) => {
    e.preventDefault()
    router.push('/')
  }

 return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
)

高阶函数导航

import { withRouter } from 'next/router'

function Page({ router }{
  return <p>{router.pathname}</p>
}

export default withRouter(Page)

路由参数

// 接参
import { withRouter } from 'next/router'

function Page({ router }) {
  return <p>{router.query}</p>
}

export default withRouter(Page)

// 跳转
import { useRouter } from 'next/router'

export default ()=>(
  const router = useRouter()
  
  const handleClick = (e) => {
    e.preventDefault()
    router.push('/index?params=123')
  }

	return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
)

中间件

在pages下创建名称为 _middleware.ts 文件, 注意, 这里的逻辑是,如果你在pages的根目录下创建中间件,那么它将拦截pages目录下的所有路径。

- package.json
- /pages
  _middleware.ts # 将在 / 页面下的所有路由上运行
  index.tsx
  about.tsx
    teams.tsx

当然 ,也可以嵌套:

- package.json
- /pages
    index.tsx
    - /about
      _middleware.ts # 首先运行
      about.tsx
      - /teams
        _middleware.ts # 随后运行
        teams.tsx

API Routes

- package.json
- /pages
 - /api
    login.ts

在pages下创建一个api文件夹,然后写关于登录的逻辑:

import type { NextApiRequest, NextApiResponse } from 'next'

export default async (req: NextApiRequest, res: NextApiResponse) => {
 // login 函数可以是你自己写的登录逻辑,也可以通过请求调其他的api
 const token = await login({ ...req })
 res.send(JSON.stringify(token, null2))
}

// 前端调 这个接口
import { useState, useEffect } from "react"

export default function Login({
  useEffect(() => {
    const fetchData = async () => {
   // /api/login 就是你在 api 目录下所创建的文件名称, 注意大小写
      const res = await fetch("/api/login")
      const json = await res.json()
      if (json.content) {
        setContent(json.content)
      }
    }
    fetchData()
  }, [session])

  return (
    <div>Login</div>
  )
}

当然 api 也支持中间件, 上面所述是用在页面的中间件, 在 api 中也是可以使用中间件的。
例如,使用一个解决跨域的中间件,需要这么做:

import Cors from 'cors'


const cors = Cors({
  methods: ['GET''HEAD'],
})

function runMiddleware(req, res, fn{
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result)
      }

      return resolve(result)
    })
  })
}

async function handler(req, res{
  // 中间件
  await runMiddleware(req, res, cors)

  // API逻辑的其余部分
  res.json({ message: 'Hello Everyone!' })
}

export default handler

渲染方式 (数据获取)

在next中有多种渲染方式:

  1. getInitialProps (官方不推荐使用, 服务端渲染 SSR)
  2. getServerSideProps (服务端渲染 SSR)
  3. CSR (客户端渲染)
  4. getStaticProps(静态页面生成 SSG)和 getStaticPaths
  5. ** ISR 递增式静态再生 新策略 Next.js v9.5 **

介绍

getInitialProps (官方不推荐使用)

function Page({ stars }{
  return <div>Next stars: {stars}</div>
}

/
/ 作为页面的静态方法
Page.getInitialProps = async (ctx) => {
  /
/ 请求数据,返回给组件,组件 props 中可以拿到数据 , ctx 中可以获取到路由信息,如 query
  const res = await fetch('https:/
/api.github.com/repos/vercel/next.js')
  const json = await res.json()
  return { stars: json.stargazers_count }
}

export default Page

getServerSideProps (服务端渲染 SSR)

在每次请求时重新渲染,通过模板引擎替换内容,返回新的html页面

import { InferGetServerSidePropsType } from 'next'

type Data = { ... }

export const getServerSideProps = async () => {
  const res = await fetch('https://.../data')
  const data: Data = await res.json()
  
  if (!data) {
    // 如果没有数据 ,重定向回 首页, 当然你也可以响应一个状态码,next 回重定向到该状态码页面
    return {
      redirect: {
        destination: '/',
        permanent: false,
        // statusCode: 302,
      },
    }
  }
  
  // 也可以直接返回 404 页面
  if (!data) {
    return {
      notFound: true,
    }
  }

  return {
    // 在组件 props 中 可以拿到 data
    props: {
      data,
    },
  }
}

function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>{
  return <div>Next data: {data}</div>
}

export default Page

CSR (客户端渲染)

这个好理解,就是前端发送请求更新渲染页面

// 客户端渲染
const PostsIndex: NextPage = () => {
    const [posts, setPosts] = useState<Post[]>([])
    useEffect(() => {
        // 请求数据
        axios.get('api/v1/posts').then(res => {
            setPosts(res.data)
        })
    }, [])
    return (
        <div>
            {posts.map(item => <ul key={item.id}>
                <li>{item.title}</li>
                <li>{item.content}</
li>
                <li>{item.date}</li>
            </u
l>)}
        </div>
    )
}

getStaticProps(静态页面生成 SSG)

动态的内容静态化, 适用于博客 、文档、电子商务等不会有不断更新的数据。

const PostsIndex: NextPage<Props> = (props) => {
    const { posts } = props
    return (
        <div>
            <h1>文章列表</h1>
            {posts}
        </
div>
    )
}

export default PostsIndex;

export const getStaticProps: GetStaticProps = async () => {
    // 请求数据
    const posts = await getPosts()
    return {
        props: {
            posts
        }
    }
}
image.png
image.png

getStaticPaths

getStaticPaths 必须跟 getStaticProps 一起使用。
它用于在使用动态路由时生成静态文件, 你可以想象, 如果你有上千个产品页面,每个页面根据id获取详情,而你又希望其中的主推产品访问速度能够更快, 这个场景就需要使用到 getStaticPaths , 它会在构建式根据数据生成主推产品的html,它有个必填属性 fallback, 它有三个值: true、false、 blocking 。

fallback: false: 如果页面相对少的情况下,在构建的时候想要一次性把所有的页面都生成,那就可以使用 false,如果在构建时没有生成的页面用户访问的情况下,会返回 404 页面。

fallback: true: 如果页面相对较多,我们只是想在构建时生成部分页面,但是它跟false的表现不一样的地方在于,如果用户访问构建时未生成的页面地址时,并不会返回 404 页面,而是会在后台生成页面后返回给用户,并将页面存放进所有的页面池,这个过程只发生在第一次访问的时候。 而我们在组件处可以很好的利用这个值,返回页面的加载状态,例如,展示个骨架屏或者 loading

fallback: blocking: 它跟 true 一样, 用户访问的时候并不会返回404页面,不同之处在于不会向用户显示后备页面(例如 loading ,骨架屏等), 因为用户在阻塞场景中等待页面加载时什么也看不到(空白);所以不建议与需要很长时间才能加载的页面一起使用。

总结: fallback: true 将是更好的用户体验,因为他们至少会在等待时看到加载微调器或骨架页面。

// pages/product/[id].js 动态路由使用 [] 表示
import fetch from 'node-fetch'

function Product({ name, json }{
 const router = useRouter()
 
 // 判断 isFallback 展示加载中的状态 , 这个值就是 getStaticPaths 中设置的 fallback 字段
 if (router.isFallback) {
  return <div>Loading...</div>
 }
 
 return <div>{name} json: {json}</
div>
}

// 首先执行。 返回路径以使用数组进行预构建。
export async function getStaticPaths({
 // 这里可以通过请求也可以读取某个文件
 const res = await fetch('https://xxxx')
 const repos = await res.json()
 // 返回路径
 const paths = repos.map(repo => `/product/${repo.id}`)
 
 // fallback 设置为 false , 表示其他路由为404 , 如果设置为true的话,则即使未预构建的路径也不会为404
 return { paths, fallback: false }
}

// 接收带有路由信息的参数
export async function getStaticProps({ params }{
 // 对应于文件名 product/[id].js
 const id = params.id
 const res = await fetch(`https://xxxx/${id}`)
 const json = await res.json()
 
 return { props: { id, json } }
}

export default Product

ISR(递增式静态再生) 渲染

资料: https://vercel.com/docs/concepts/next.js/incremental-static-regeneration
Next.js 允许你在构建站点_后创建或更新静态页面。_
增量静态重新生成 (ISR) 使你能够在每页的基础上使用静态生成,而无需重建整个站点
使用 ISR,你可以在扩展到数百万页的同时保留静态的优势。

// pages/products/[id].js

export async function getStaticProps({ params }{
  return {
    props: {
      product: await getProductFromDatabase(params.id)
    },
    revalidate: 60 // 实现 ISR 
  }
}

水合错误

在看完Next 文档以及众多资料后,了解到 ssr 渲染的原理是经过了两次编译过程,服务端编译生成html返回给前端,前端在请求到 js 后,会再次进行一次渲染处理事件,这个过程叫做水合, 而这个过程可能会发生错误(React Hydration Error),而这个错误的产生是因为 在渲染应用程序时,预渲染的 React 树 (SSR/SSG) 与在浏览器中首次渲染期间渲染的 React 树之间存在差异。第一个渲染称为 Hydration,它是 React 的一个特性。
这可能会导致 React 树与 DOM 不同步,并导致出现意外的内容/属性。

示例:

function MyComponent({
 // 这个条件取决于`window`是否存在 。
 // 而在浏览器的第一次渲染期间,`color` 变量会有所不同
 const color = typeof window !== 'undefined' ? 'red' : 'blue'
 // 由于 `color` 作为变量传递,因此服务器端渲染的内容与第一次渲染中渲染的内容之间存在不匹配
 return <h1 className={`title ${color}`}>Hello World!</h1>
}

解决问题:

// 为了防止第一次渲染不同,您可以使用仅在浏览器中执行并在水合期间执行的“使用效果”
import { useEffect, useState } from 'react'
function MyComponent({
  // 默认值为'blue',它将在预渲染和浏览器中的第一次渲染(水化)期间使用
  const [color, setColor] = useState('blue')

 // 在水化过程中调用了`use Effect`。 `window` 在 `use Effect` 中可用。在这种情况下,因为我们知道我们在浏览器中检查窗口是不需要的。如果您需要从窗口中读取内容,那很好。
// 通过在 `use Effect` 中调用 `set Color`,在 hydrating 后触发渲染,这会导致“浏览器特定”值可用。在这种情况下,“红色”。
  useEffect(() => setColor('red'), [])
  // 由于颜色是作为道具传递的状态,因此服务器端渲染的内容与第一次渲染中渲染的内容之间没有不匹配。使用效果运行后,颜色设置为“红色”
  return <h1 className={`title ${color}`}>Hello World!</h1>
}

更多资料
https://github.com/reactwg/react-18/discussions/37
https://nextjs.org/docs/messages/react-hydration-error
https://www.benmvp.com/blog/handling-react-server-mismatch-error/
https://github.com/facebook/react/issues/15446
https://atomizedobjects.com/blog/react/what-is-hydration-in-react-based-applications/

Next 实现登录

在 web 应用中经常需要验证使用者的权限,例如登入与未登入能看到的页面可能会不同,需要通过一些方式验证使用者是否能夠进入该页面。
在 Next.js 中对于 SSG 与 SSR 有不同的验证方式,以下将会以 next-auth 這個库为主轴,介绍在 SSG 与 SSR 的页面中如何做验证。

在官网中还提到过一个库是使用 iron-session 来做验证的,大家可以自行查看文档了解这个包的使用。

在 SSR(服务端渲染)、SSG(静态全量渲染 , 本文 ISR也包括在 SSG 当中) 、 CSR(客户端渲染) 中验证权限的方式

在 Next.js 由于可以混用 SSR、SSG 与 CSR 三种不同的页面,所以针对不同的页面,也会有不同的验证方式。

SSG 和 CSR 的验证方式很接近,都是在用戶端验证,如果验证失败则可以使用 next/router 重定向到其他页面;而 SSR 则是在服务器端验证,验证失败则可以直接在 getServerSideProps 重定向到其他页面。

web vitals 的角度來看,由于 SSR 會在服务器端做验证,所以验证阶段必须越快越好,否则会影响 TTI (Time to Interactive) 与 TTFB (Time to First Byte)。

如果验证很慢,可以考虑将页面转换成 SSG 的形式,有利于提升用户体验。

next-auth 简介

next-auth 是一个提供完整解決方案的 npm 包,包括支持 Google、 Facebook、 Twitter 等第三方登入的方法,也可以使用自定义的验证方式,支持 OAuth 1.0、1.0A 与 2.0 的验证,也同時支持 JSON Web Token 与 database session 等的验证模式,是完整性很高的优秀库。

next-auth 的 API 与官网提到的 iron-session 比较起來更容易阅读,不仅提供 hook 的 API,而且还有提供在用戶端服务器端都能使用的 **getSession **,让使用者验证更容易实现。

安装 next-auth

npm i next-auth

配置 .env

NEXTAUTH_URL=http://localhost:3000/  # next-auth url

NEXTAUTH_SECRET=            # next-auth secret , 用于加密 jwt ,本文不需要生成jwt 而是通过 业务服务器请求回来的

GITHUB_ID=              # GITHUB_ID ,这个需要在 github 开发者页面下获取
GITHUB_SECRET=            # GITHUB_SECRET , 这个是 secret

API_URL=http://localhost:3001/    # axios baseUrl

本文中还实现了github 授权登录,所以需要在 github 中创建一个应用,如果不知道如何开启开发者授权登陆,可以查看下图右上角路径找到,具体自行查阅资料,本文就不多赘述这部分内容了。
image.png

注入 Provider

在 _app.tsx 文件中注入 session , 这里需要特别说明下,refetchInterval 字短用于轮询 cookie , 如果你在 callbacks 回调中使用了token 过期的操作,你需要在这里指定一个比token过期时间低的数字,它会在这期间轮询,防止 token 过期后的副作用

import { SessionProvider } from 'next-auth/react'
import Head from 'next/head'

import '@styles/globals.css'
import '@styles/antd.less'

import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
	return (
		<>
			<Head>
				<title>React-Nextjs-Template</title>
				<meta charSet="utf-8" />
				<meta
					name="viewport"
					content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
					/>
			</Head>
			<SessionProvider refetchInterval={10} session={session}>
				<Component {...pageProps} />
			</SessionProvider>
		</>
	)
}

export default MyApp

登录

新建 api/auth/[...nextauth].ts 动态路由,这里是 next-auth 的中心舞台

import NextAuth from 'next-auth'
import GithubProvider from 'next-auth/providers/github'
import CredentialsProvider from 'next-auth/providers/credentials'

import { GET, POST } from '@lib/axios'

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function auth(req: NextApiRequest, res: NextApiResponse{
  const providers = [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    CredentialsProvider({
      name: 'myProject',
      credentials: {
        email: {
          label: 'email',
          type'text',
          placeholder: 'email',
        },
        password: { label: '密码'type'password' },
      },
      async authorize(credentials, req) {
        try {
          const response = await POST('/auth/login', {
            email: credentials?.email,
            password: credentials?.password,
          })

          const response_user = await GET('/me'undefined, {
            headers: { Authorization: `Bearer ${response.data.accessToken}` },
          })
          
          // 登录成功后需要return你需要存储在 cookie 当中的数据
          if (response_user.data)
            return {
              ...response_user.data,
              accessToken: response.data.accessToken,
              refreshToken: response.data.refreshToken,
              accessTokenExpires: response.data.expiresIn,
            }

          return null
        } catch (error) {
          return null
        }
      },
    }),
  ]

  return await NextAuth(req, res, {
    providers,
    callbacks: {
      // 每次调用 getSession() 、useSession() 的时候 都会触发并将 token 存入 user 中
      async session({ session, token }) {
        session.user.accessToken = token.accessToken
        session.user.refreshToken = token.refreshToken
        session.error = token.error // 用于处理token 失效

        return session
      },

      // 上面登录成功后,jwt 回调会执行, user 中会拿到你 return 出来的数据, 注意: 仅在你调用 signin 接口的时候才会有值,之后都是在 cookie中读取
      async jwt(params) {
        const { token, user, account } = params

        // 判断是自定义登录方式 credentials , 并且是在登录页面调 signin 接口的时候 才会有 user信息
        // 如果有user信息,证明用户正在进行登录,所以需要将数据保存持久化到 cookie 中, 这样 session 回调就能拿到数据了
        if (account?.type === 'credentials' && user) {
          token.accessToken = user.accessToken as string
          token.refreshToken = user.refreshToken as string
          token.accessTokenExpires =
            Date.now() + (user.accessTokenExpires as number) * 1000 // 设置过期时间 ,这里需要跟后端开发对接询问过期时间,或者直接叫后端返回 expiresAt 字段告诉我们过期时间 , 目的是问了让 next-auth  token 同步服务端 token
          token.userRole = 'admin'
        }
        
        
        // 判断 token的有效期, 这里有个注意点,token 的有效期需要跟后端沟通,
        // 直接告诉你或者加个字段告诉你过期时间,这么做的目的是,next-auth 的 token 过期是独立的,
        // 并且是每次用户活跃都会刷新token的时间,这样就会出现跟后端服务的token过期不同步的情况,
        // 所以这里的方案是直接用服务端的token作为过期信号
        if (Date.now() < token.accessTokenExpires!) {
          return token
        }

        // 如果 登录信息过期, 希望无感刷新登录, 这里可以处理
        return {
          ...token,
          error: 'accessTokenExpiresError'// 返回给客户端,让客户端处理退出登录的问题
        }
      },
    },
    session: {
      strategy: 'jwt'// 如果这里设置的不是 jwt,那么 jwt 回调函数不会触发
    },
    pages: {
      // signIn: '/auth/signin',
      // signOut: '/auth/signout',
    },
    secret: process.env.NEXTAUTH_SECRET,
    debug: process.env.NODE_ENV === 'development',
  })
}

NextAuth 函数实现了重载, 你可以只传递** options**,也可以将 Next 请求的 req 和 res 传入。

providers 字段

providers 字段, 接收一个数组,这里面内置了很多的第三方登录,有Github, Twitter, Google等,具体查看文档, 它还支持 email 验证登录,还有 jwt 登录。

我这里使用了 github 登录 以及 jwt 登录。

GithubProvider 字段是 next-auth 内置的登录方式,你只需要传入你的 clientId 和 clientSecret 即可完成登录,当让,你也可以自定义登陆,具体查看文档,或者查看 next-auth/providers/github 这个库的源码,然后模仿实现你想要登录的第三方系统即可。

CredentialsProvider 字段是用来做 jwt 登录的;

  • name 是唯一值;
  • credentials 字段是用来定义表单的字段,我的接口需要两个参数, email 和 password ;
  • authorize 字段是用来实现你的具体登录逻辑的,它是个函数, 有两个参数,第一个参数是前端传递过来的数据,第二个是 next 请求。

callbacks 字段

在验证登录成功后,无论是在服务端或是在客户端都可以获取登录成功后的用户信息,next 提供了 getSession 和 useSession 。

  • getSession 在服务端渲染的时候使用
  • useSession 在客户端组件中使用

但是由于 next-auth 中默认的session 包含的信息是固定的,我们可以从预设的类型定义中查到默认的数据是哪些:

export interface DefaultSession extends Record<string, unknown> {
 user?: {
  name?: string | null;
  email?: string | null;
  image?: string | null;
 };
 expires: ISODateString;
}

如上可以知道,我们需要在 session 中塞入我们在登录接口中获取到的 accessToken (任何额外的数据都可以),这时候就需要用到 callbacks 回到来处理了。
在callbacks中分别定义两个函数: session 和 jwt

记住,它们的执行顺序是先 jwt 执行, 然后才是 session。

想要了解更多请查看文档

读取 token 鉴权

/api 目录下

// /api/examples/protected.ts
import { getSession } from "next-auth/react"
import type { NextApiRequest, NextApiResponse } from "next"

export default async (req: NextApiRequest, res: NextApiResponse) => {
  // getSession 可以获取到 session
  const session = await getSession({ req })

  if (session) {
    res.send({
      content:
        "This is protected content. You can access this content because you are signed in.",
    })
  } else {
    res.send({
      error: "You must be signed in to view the protected content on this page.",
    })
  }
}

然后前端调用这个接口:

import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"

export default function ProtectedPage({
  const { data: session, status } = useSession()
  const loading = status === "loading"
  const [content, setContent] = useState()

  
  useEffect(() => {
    const fetchData = async () => {
      // 客户端渲染, 调接口拿数据
      const res = await fetch("/api/examples/protected")
      const json = await res.json()
      if (json.content) {
        setContent(json.content)
      }
    }
    fetchData()
  }, [session])

  
  if (typeof window !== "undefined" && loading) return null

  // If no session exists, display access denied message
  if (!session) {
    return (
      <Layout>
        <AccessDenied />
      </Layout>
    )
  }

  
  return (
    <Layout>
      <h1>Protected Page</
h1>
      <p>
        <strong>{content ?? "\u00a0"}</strong>
      </
p>
    </Layout>
  )
}

getServerSideProps (或其他的服务端渲染的api)

import { useSession, getSession } from "next-auth/react"
import Layout from "../components/layout"
import type { NextPageContext } from "next"

export default function ServerSidePage({
  
  // 前端直接获取 data
  const { data: session, status } = useSession()
  const loading = status === "loading"

  return (
    <Layout>
      <h1>Server Side Rendering</h1>
      <p>
        This page uses the universal <strong>getSession()</
strong> method in{" "}
        <strong>getServerSideProps()</strong>.
      </
p>
      <p>
        Using <strong>getSession()</strong> in{" "}
        <strong>getServerSideProps()</
strong> is the recommended approach if you
        need to support Server Side Rendering with authentication.
      </p>
      <p>
        The advantage of Server Side Rendering is this page does not require
        client side JavaScript.
      </
p>
      <p>
        The disadvantage of Server Side Rendering is that this page is slower to
        render.
      </p>
    </
Layout>
  )
}

// 也可以在 getServerSideProps 中获取
export async function getServerSideProps(context: NextPageContext{
  return {
    props: {
      session: await getSession(context),
    },
  }
}

pages/_middleware.ts 中间件

import { withAuth } from 'next-auth/middleware'

// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
  callbacks: {
    authorized: ({ token }) => token?.userRole === 'admin',
  },
})

你甚至可以在 axios 的拦截器中控制:

import axios from 'axios'
import { notification } from 'antd'
import type { AxiosRequestConfig } from 'axios'
import { stringify } from 'qs'
import { getSession } from 'next-auth/react'

import retryAdapterEnhancer from './retryAdapter'

export interface IAxiosRequestConfig extends AxiosRequestConfig {
  retryCount?: number
  retryDelay?: number
}

// export interface IResponse<T> {
//   code: number
//   data: T
//   message?: string
// }

const baseURL = process.env.API_URL || 'http://localhost:3001'

export const request = axios.create({
  baseURL,
  adapter: retryAdapterEnhancer(axios.defaults.adapter!, {
    retryDelay: 1000,
  }),
})

// 请求拦截`
request.interceptors.request.use(async (config) => {
  const session = await getSession()

  if (session) {
    config.headers!.Authorization = `Bearer ${session.jwt}`
  }

  return config
})

// 响应拦截
request.interceptors.response.use((response) => {
  const {
    data,
    data: { errors },
    status,
  } = response

  if (status === 200 && errors && typeof window !== undefined) {
    notification.error({
      message: '请求错误',
      description: JSON.stringify(errors),
    })

    return Promise.reject(errors)
  }

  return data
})

export const GET = <T>(
  URL: string,
  params?: any,
  config?: AxiosRequestConfig
): Promise<T> =>
  request.get(params ? `${URL}?${stringify(params)}` : URL, config)

export const POST = <T>(
  URL: string,
  params?: any,
  config?: AxiosRequestConfig
): Promise<T> => request.post(URL, params, config)

export const PUT = (URL: string, params?: any) => request.put(URL, params)

export const DELETE = (URL: string, params?: any) => request.delete(URL, params)

是不是很酷!!!

自定义登录登出页面

在默认情况下, next-auth 提供了默认的登录页面,你也可以通过在配置中实现自定义登录页面:

 pages: {
	// signIn: '/auth/signin',
	// signOut: '/auth/signout',
},

最后附上 github 地址 , 喜欢的各位点个 start 🌟

如果我的文章帮助到了你,希望你能个我点个赞 👍
谢谢 🙏

参考文档:
https://nextjs.org/docs/messages/react-hydration-error
https://zhuanlan.zhihu.com/p/47044039
https://github.com/reactwg/react-18/discussions/37
https://ithelp.ithome.com.tw/articles/10275078


分类:

前端

标签:

前端

作者介绍

l
lynn1286
V1