第九章 NextAuth实现身份验证与短信登录

NextAuth.js 是一个针对Next.js应用程序的完整开源身份验证解决方案,在Next.js中进行身份验证非常灵活且方便

NextAuth.js 旨在与任何 OAuth 服务配合使用,它支持OAuth 1.01.0A2.0OpenID Connect (OIDC) ,并内置对最流行的登录服务的支持。

nextAuth官网:next-auth.js.org/

他支持用户可以通过四种方式登录:

接下来实践中使用的是使用凭证的方式进行登录

next-auth.js.org/configurati...

一、账号登录

首先在next项目安装next-auth

bash 复制代码
npm i next-auth

在我们的handleSubmitEmail函数中,我们已经成功地收集了用户的登陆信息。接下来,我们将利用NextAuth.js内置的signIn方法,来处理用户的登录请求。

signIn方法需要我们按照特定的格式传入用户的登录信息,然后它将自动处理用户的登录请求,并返回响应结果。这个过程非常简单和安全,我们无需手动处理复杂的登录逻辑,NextAuth.js会为我们处理所有的细节。

ts 复制代码
// import
import { TipsCode, getMessages } from '@/lib/tips'
import { signIn } from 'next-auth/react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useToast } from '@/components/ui/use-toast'

// value
const router = useRouter()
const searchParams = useSearchParams()
const callbackUrl = searchParams.get('callbackUrl')
const { toast } = useToast()

// login
const handleSubmitEmail = useCallback(
    async ({ email, password }: TEmailLoginVaildator) => {
      setLogging(true)
      try {
        const response = await signIn('credentials', {
          email,
          password,
          loginType: 'email',
          phoneCode: '',
          redirect: false,
        })
        if (!response?.error) {
          router.push(callbackUrl || '/')
          router.refresh()
          toast({
            title: getMessages('10030'),
            description: getMessages('10034'),
            variant: 'default',
          })
        } else {
          toast({
            title: getMessages('10030'),
            description: getMessages(response.error as TipsCode),
            variant: 'destructive',
          })
        }
      } catch (error) {
        console.log(error)
      }
      setLogging(false)
    },
    [callbackUrl, router, toast]
  )

使用next-auth前需要初始化NextAuth,Next13开始引入了Route Handlers,这是在 App Router ( ) 中处理类似 REST 的请求的首选方式app/。我们可以使用路由处理程序初始化 NextAuth.js,这与 API 路由非常相似。创建/app/api/auth/[...nextauth]/route.ts前我们需要先在.env.local增加两个环境变量NEXTAUTH_URLNEXTAUTH_SECRET

使用以下命令生成secret

bash 复制代码
# 生成了 32 字节(或 256 位)的随机数据,并以 base64 格式输出。这种类型的随机数据经常用于生成密钥、盐值等安全相关的信息。
openssl rand -base64 32

.env.example示例如下

ini 复制代码
# next-auth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET=".."

二、初始化程序

开始创建初始化程序

  1. session:定义了会话管理的方式。在这个例子中,使用的是 jwt 策略,它使用 JSON Web Tokens 来管理会话。maxAge 参数定义了会话的最大有效期,单位是秒。
  • pages:定义了各种身份验证相关页面的路径。在这个例子中,定义了登录页面的路径为 /login,即在拦截用户登录后会重定向到该路径
  • providers:定义了可以用于身份验证的方法。在这个例子中,使用的是 CredentialsProvider,它允许用户通过提供某些凭证(如用户名和密码)进行身份验证
  • authorize:用于验证用户的凭证。在这个例子中,这个函数首先检查登录类型,然后根据类型(电子邮件或手机号)验证用户的凭证。如果凭证有效,函数将返回一个用户对象,否则将抛出一个错误,credentials 对象定义了用于身份验证的凭证的类型,设置为空对象 {},主要是为了表明这些键存在,而它们的具体值将在运行时通过 authorize 函数接收
  • callbacks:next-auth.js.org/configurati...
  • 在内部,NextAuth.js 检测到它正在 Route Handler 中初始化(通过了解它传递了一个 WebRequest实例),并将返回一个返回Response实例的处理程序。路由处理程序文件期望您导出一些处理请求并返回响应的命名处理程序函数。NextAuth.js 需要GETPOST处理程序才能正常运行,因此我们导出这两个。
ts 复制代码
// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { compare } from 'bcryptjs'
import { db } from '@/db'
import type { AuthOptions } from 'next-auth'

export const authOptions: AuthOptions = {
  session: {
    strategy: 'jwt',
    maxAge: 2 * 24 * 60 * 60,
  },
  pages: {
    signIn: '/login', // 重定向到 /login
  },
  providers: [
    CredentialsProvider({
      credentials: {
        email: {},
        password: {},
        loginType: {},
        phone: {},
        phoneCode: {},
      },
      async authorize(credentials) {
        const { email, password, loginType, phone, phoneCode } =
          credentials || {}
        if (loginType === 'email') {
          if (!email || !password) throw new Error('10012')
          const user = await db.user.findFirst({
            where: {
              email,
            },
          })
          if (!user) throw new Error('10031')
          // 停用账号禁止登录
          if (!user?.active) throw new Error('10032')
          const passwordCorrect = await compare(password, user.password!)
          if (passwordCorrect) {
            return {
              id: user.id,
              email: user.email,
              phone: user.phone,
              name: user.name,
              image: user.image,
            }
          } else {
            throw new Error('10033')
          }
        }
        if (loginType === 'phone') {
          if (!phone || !phoneCode) throw new Error('10012')
          const user = await db.user.findFirst({
            where: {
              phone,
            },
          })
          if (!user) throw new Error('10031')
          // 停用账号禁止登录
          if (!user?.active) throw new Error('10032')
          const phoneCodeRecord = await db.activateToken.findFirst({
            where: {
              account: phone,
            },
          })
          if (!phoneCodeRecord) throw new Error('10022')
          // 验证码过期
          if (phoneCodeRecord.expiredAt.getTime() < Date.now()) {
            throw new Error('10023')
          }
          if (phoneCodeRecord.code === phoneCode) {
            return {
              id: user.id,
              email: user.email,
              phone: user.phone,
              name: user.name,
              image: user.image,
            }
          } else {
            throw new Error('10024')
          }
        }
        return null
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.phone = user.phone
      }
      return token
    },
    async session({ session, token }) {
      if (token && session.user) {
        session.user.id = token.id
        session.user.phone = token.phone
      }
      return session
    },
  },
}

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

代码中我们使用了phone,原始的User类型并不支持,我们需要扩展 next-auth 库的 UserSessionJWT 接口,这是 TypeScript 的一个特性,允许你扩展现有的类型定义,以适应你的特定需求。这对于使用第三方库非常有用,因为你可以在不修改库源代码的情况下,添加你需要的新功能。

以下配置可以让 TypeScript 知道 UserSessionJWT 对象可能包含 phone 属性。这样,在你的代码中使用这些对象时,如果你尝试访问 phone 属性,TypeScript 就不会报错,因为它知道这个属性是存在的,当需要规则或者更多信息自行扩展。

ts 复制代码
// types/next-auth.d.ts
import { DefaultSession, DefaultUser } from 'next-auth'
export enum Role {
  user = 'user',
  admin = 'admin',
}
// if you want to add more fields to the user
interface IUser extends DefaultUser {
  /**
   * Role of user
   */
  //   role?: Role
  /**
   * more fields
   */
  phone?: string | null
}
declare module 'next-auth' {
  interface User extends IUser {}
  interface Session {
    user?: User
  }
}
declare module 'next-auth/jwt' {
  interface JWT extends IUser {}
}

三、短信登录

在我们的handleSubmitPhone函数中,我们已经成功地收集了用户的手机号和验证码信息。同样我们将利用NextAuth.js内置的signIn方法,来处理用户的登录请求。

ts 复制代码
// import
import { signIn } from 'next-auth/react'
import { useRouter, useSearchParams } from 'next/navigation'

// value
const router = useRouter()
const searchParams = useSearchParams()
const callbackUrl = searchParams.get('callbackUrl')

// login
 const handleSubmitPhone = useCallback(
    async ({ phone, phoneCode }: TPhoneFormVaildator) => {
      setLogging(true)
      try {
        const response = await signIn('credentials', {
          loginType: 'phone',
          phone,
          phoneCode,
          redirect: false,
        })
        if (!response?.error) {
          router.push(callbackUrl || '/')
          router.refresh()
          toast({
            title: getMessages('10030'),
            description: getMessages('10034'),
            variant: 'default',
          })
        } else {
          toast({
            title: getMessages('10030'),
            description: getMessages(response.error as TipsCode),
            variant: 'destructive',
          })
        }
      } catch (error) {
        console.log(error)
      }
      setLogging(false)
    },
    [callbackUrl, router, toast]
  )

四、添加提示信息

以上代码涉及到的文案提示如下:

arduino 复制代码
'10031': '账号未注册,前往注册',
'10032': '账号已停用,请联系管理员',
'10033': '邮箱或密码不正确',
'10034': '登录成功',

五、验证用户

一、首页登录后的用户不允许访问登录注册页面

以登录页为例,如果有用户session就重定向,注册页同

ts 复制代码
// src/app/login/page.tsx
import { getServerSession } from 'next-auth'
import Form from './form'
import { redirect } from 'next/navigation'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
// 注册页只是方法名不同=》 RegisterPage
export default async function LoginPage() {
  const session = await getServerSession(authOptions)
  if (session) {
    redirect('/')
  }
  return <Form />
}

二、我们有一些保护页面,如dashboard,没有登录不允许访问

next-auth.js.org/configurati...

我们使用nextAuth的中间件功能,创建src/middleware.ts文件,如果dashboard有动态路由就使用以下方式就可以拦截了

ts 复制代码
export { default } from 'next-auth/middleware'

export const config = { matcher: ['/dashboard/:path*'] }

登录后在没有登出逻辑前如何测试登出来测试中间的拦截效果呢,步骤如下

  1. chrome控制台
  2. 应用选项
  3. Cookie选项
  4. 执行清除所有cookie

后面在导航里将增加手动退出登录。

相关推荐
王哈哈^_^1 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie1 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具2 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事4 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶4 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json