第九章 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

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

相关推荐
Jiaberrr2 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy2 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白2 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、2 小时前
Web Worker 简单使用
前端
web_learning_3212 小时前
信息收集常用指令
前端·搜索引擎
tabzzz3 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百3 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao3 小时前
自动化测试常用函数
前端·css·html5
码爸3 小时前
flink doris批量sink
java·前端·flink
深情废杨杨3 小时前
前端vue-父传子
前端·javascript·vue.js