NextAuth.js 是一个针对Next.js应用程序的完整开源身份验证解决方案,在Next.js中进行身份验证非常灵活且方便
NextAuth.js 旨在与任何 OAuth 服务配合使用,它支持OAuth 1.0 、1.0A 、2.0 和OpenID Connect (OIDC) ,并内置对最流行的登录服务的支持。
nextAuth官网:next-auth.js.org/
他支持用户可以通过四种方式登录:
- 使用内置的 OAuth 提供程序(例如 Github、Twitter、Google 等...)
- 使用自定义 OAuth 提供程序
- 使用电子邮件
- 使用凭证
接下来实践中使用的是使用凭证的方式进行登录
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_URL
与NEXTAUTH_SECRET
使用以下命令生成secret
bash
# 生成了 32 字节(或 256 位)的随机数据,并以 base64 格式输出。这种类型的随机数据经常用于生成密钥、盐值等安全相关的信息。
openssl rand -base64 32
.env.example
示例如下
ini
# next-auth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET=".."
二、初始化程序
开始创建初始化程序
- session:定义了会话管理的方式。在这个例子中,使用的是
jwt
策略,它使用 JSON Web Tokens 来管理会话。maxAge
参数定义了会话的最大有效期,单位是秒。
- pages:定义了各种身份验证相关页面的路径。在这个例子中,定义了登录页面的路径为
/login
,即在拦截用户登录后会重定向到该路径 - providers:定义了可以用于身份验证的方法。在这个例子中,使用的是
CredentialsProvider
,它允许用户通过提供某些凭证(如用户名和密码)进行身份验证 - authorize:用于验证用户的凭证。在这个例子中,这个函数首先检查登录类型,然后根据类型(电子邮件或手机号)验证用户的凭证。如果凭证有效,函数将返回一个用户对象,否则将抛出一个错误,
credentials
对象定义了用于身份验证的凭证的类型,设置为空对象{}
,主要是为了表明这些键存在,而它们的具体值将在运行时通过authorize
函数接收 - callbacks:next-auth.js.org/configurati...
- 在内部,NextAuth.js 检测到它正在 Route Handler 中初始化(通过了解它传递了一个 Web
Request实例
),并将返回一个返回Response实例
的处理程序。路由处理程序文件期望您导出一些处理请求并返回响应的命名处理程序函数。NextAuth.js 需要GET
和POST
处理程序才能正常运行,因此我们导出这两个。
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
库的 User
、Session
和 JWT
接口,这是 TypeScript 的一个特性,允许你扩展现有的类型定义,以适应你的特定需求。这对于使用第三方库非常有用,因为你可以在不修改库源代码的情况下,添加你需要的新功能。
以下配置可以让 TypeScript 知道 User
、Session
和 JWT
对象可能包含 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*'] }
登录后在没有登出逻辑前如何测试登出来测试中间的拦截效果呢,步骤如下
- chrome控制台
- 应用选项
- Cookie选项
- 执行清除所有cookie
后面在导航里将增加手动退出登录。