mcpField
A
authjs-skills

authjs-skills

github.com/gocallum开发23793 次安装40 周前更新
代码仓库

关于 authjs-skills

authjs-skills 汇总 Auth.js v5 的配置与使用指南,面向在 Next.js 应用中实现身份验证的开发者。内容涵盖提供方、会话、路由保护、数据库适配器及从 v4 迁移。

主要功能

  1. 01Auth.js v5 基础配置
  2. 02Google 与凭据提供方
  3. 03会话和回调管理
  4. 04路由保护与角色控制
  5. 05数据库适配器配置
  6. 06v4 迁移说明

适用场景

  1. 01为 Next.js 应用配置登录
  2. 02接入 OAuth 或用户名密码
  3. 03保护页面和 API 路由
  4. 04从 Auth.js v4 升级

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gocallum/nextjs16-agent-skills --skill authjs-skills

安装后在 Claude Code 中输入/authjs-skills确认已加载。

详细介绍

链接

安装

pnpm add next-auth@beta

注意:Auth.js v5 目前处于 beta 阶段。使用 next-auth@beta 安装最新的 v5 版本。

Auth.js v5 有哪些新变化?

相较于 v4 的主要变化
  • 简化配置:设置更加精简,并提供更好的 TypeScript 支持
  • 通用的 auth() 导出:使用单个函数在所有上下文中进行身份验证
  • 增强安全性:改进 CSRF 防护和会话处理
  • 支持 Edge Runtime:完全兼容 Edge Runtime 和中间件
  • 更好的类型安全性:全面改进 TypeScript 定义

环境变量

必需的环境变量
# Auth.js Configuration
AUTH_SECRET=your_secret_key_here

# Google OAuth (if using Google provider)
AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secret

# For production deployments
AUTH_URL=https://yourdomain.com

# For development (optional, defaults to http://localhost:3000)
# AUTH_URL=http://localhost:3000
生成 AUTH_SECRET
# Generate a random secret (Unix/Linux/macOS)
openssl rand -base64 32

# Alternative using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

# Using pnpm
pnpm dlx auth secret

重要:切勿将 AUTH_SECRET 提交到版本控制中。开发时请使用 .env.local。

基本设置(Next.js App Router)

1. 创建 auth.ts 配置文件

在项目根目录(与 package.json 同级)创建 auth.ts:

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        // TODO: Implement your authentication logic here
        // This is a basic example - see Credentials Provider section below for complete implementation
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        // Example: validate against database (placeholder)
        // See "Credentials Provider" section for full implementation with bcrypt
        const user = { id: "1", email: credentials.email, name: "User" } // Replace with actual DB lookup
        
        if (!user) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
        }
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin',
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // Return true if user is authenticated
      return !!auth
    },
  },
})

注意:这是一个基本设置示例。有关可用于生产环境的凭据身份验证,请参阅下方的“Credentials Provider”部分,其中包含使用 bcrypt 进行适当的密码哈希处理以及数据库集成。

2. 创建 API 路由处理程序

创建 app/api/auth/[...nextauth]/route.ts:

import { handlers } from "@/auth"

export const { GET, POST } = handlers
3. 添加中间件(可选,但建议添加)

在项目根目录创建 middleware.ts:

export { auth as middleware } from "@/auth"

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

如需更多控制:

import { auth } from "@/auth"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
  
  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL('/auth/signin', req.url))
  }
})

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

Google OAuth Provider

1. 设置 Google Cloud Console
  1. 前往 Google Cloud Console
  2. 创建新项目或选择现有项目
  3. 启用 Google+ API
  4. 创建 OAuth 2.0 凭据:
    • 应用类型:Web 应用
    • 授权的重定向 URI:
      • 开发环境:http://localhost:3000/api/auth/callback/google
      • 生产环境:https://yourdomain.com/api/auth/callback/google
  5. 将 Client ID 和 Client Secret 复制到 .env.local
2. 配置
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code"
        }
      }
    }),
  ],
})
3. Google Provider 选项
Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  // Request additional scopes
  authorization: {
    params: {
      scope: "openid email profile",
      prompt: "select_account", // Force account selection
    }
  },
  // Allow specific domains only
  allowDangerousEmailAccountLinking: false,
})

Credentials Provider(用户名/密码)

必需的依赖项
# Install required packages for credentials provider
pnpm add bcryptjs zod
pnpm add -D @types/bcryptjs
1. 基本配置
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { z } from "zod"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email", placeholder: "[email protected]" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        try {
          const { email, password } = credentialsSchema.parse(credentials)
          
          // Fetch user from database
          const user = await prisma.user.findUnique({
            where: { email },
          })

          if (!user) {
            throw new Error("User not found")
          }

          // Verify password
          const isValidPassword = await bcrypt.compare(password, user.hashedPassword)
          
          if (!isValidPassword) {
            throw new Error("Invalid password")
          }

          // Return user object (must include id)
          return {
            id: user.id,
            email: user.email,
            name: user.name,
            image: user.image,
          }
        } catch (error) {
          console.error("Authentication error:", error)
          return null
        }
      },
    }),
  ],
  session: {
    strategy: "jwt", // Required for credentials provider
  },
})
2. 用户注册示例
// app/api/auth/register/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { z } from "zod"
import { prisma } from "@/lib/prisma"

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2),
})

export async function POST(req: Request) {
  try {
    const body = await req.json()
    const { email, password, name } = registerSchema.parse(body)

    // Check if user exists
    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "User already exists" },
        { status: 400 }
      )
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10)

    // Create user
    const user = await prisma.user.create({
      data: {
        email,
        name,
        hashedPassword,
      },
    })

    return NextResponse.json(
      { message: "User created successfully", userId: user.id },
      { status: 201 }
    )
  } catch (error) {
    console.error("Registration error:", error)
    return NextResponse.json(
      { error: "Failed to register user" },
      { status: 500 }
    )
  }
}

在组件中使用 Auth

服务端组件
import { auth } from "@/auth"

export default async function ProfilePage() {
  const session = await auth()

  if (!session?.user) {
    return <div>Not authenticated</div>
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
    </div>
  )
}
服务端操作
"use server"

import { auth } from "@/auth"
import { revalidatePath } from "next/cache"
import { prisma } from "@/lib/prisma"

export async function updateProfile(formData: FormData) {
  const session = await auth()

  if (!session?.user) {
    throw new Error("Not authenticated")
  }

  const name = formData.get("name") as string

  // Update database
  await prisma.user.update({
    where: { id: session.user.id },
    data: { name },
  })

  revalidatePath("/profile")
}
客户端组件(使用 SessionProvider)
// app/providers.tsx
"use client"

import { SessionProvider } from "next-auth/react"

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}
// app/layout.tsx
import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
// app/components/user-profile.tsx
"use client"

import { useSession, signIn, signOut } from "next-auth/react"

export function UserProfile() {
  const { data: session, status } = useSession()

  if (status === "loading") {
    return <div>Loading...</div>
  }

  if (!session) {
    return (
      <button onClick={() => signIn()}>
        Sign In
      </button>
    )
  }

  return (
    <div>
      <p>Signed in as {session.user?.email}</p>
      <button onClick={() => signOut()}>
        Sign Out
      </button>
    </div>
  )
}

登录/登出操作

以编程方式登录
import { signIn } from "@/auth"

// Server Action
export async function handleSignIn(provider: string) {
  "use server"
  await signIn(provider)
}

// With credentials
export async function handleCredentialsSignIn(formData: FormData) {
  "use server"
  await signIn("credentials", formData)
}

// With redirect
export async function handleGoogleSignIn() {
  "use server"
  await signIn("google", { redirectTo: "/dashboard" })
}
登录表单组件
// app/auth/signin/page.tsx
import { signIn } from "@/auth"

export default function SignInPage() {
  return (
    <div>
      <h1>Sign In</h1>
      
      {/* Google OAuth */}
      <form
        action={async () => {
          "use server"
          await signIn("google")
        }}
      >
        <button type="submit">Sign in with Google</button>
      </form>

      {/* Credentials */}
      <form
        action={async (formData) => {
          "use server"
          await signIn("credentials", formData)
        }}
      >
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Sign In</button>
      </form>
    </div>
  )
}
登出
import { signOut } from "@/auth"

export default function SignOutButton() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}

会话管理

会话策略

Auth.js v5 支持两种会话策略:

  1. JWT(默认):将会话存储在加密的 JWT 令牌中
  2. 数据库:将会话存储在数据库中
export const { handlers, signIn, signOut, auth } = NextAuth({
  session: {
    strategy: "jwt", // or "database"
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
  },
})
扩展会话
import NextAuth from "next-auth"
import type { DefaultSession } from "next-auth"

declare module "next-auth" {
  interface Session {
    user: {
      id: string
      role: string
    } & DefaultSession["user"]
  }
}

export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      return token
    },
    session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
})

回调

必要的回调
export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    // Called when user signs in
    async signIn({ user, account, profile }) {
      // Return true to allow sign in, false to deny
      // Example: Check if email is verified
      if (account?.provider === "google") {
        return profile?.email_verified === true
      }
      return true
    },

    // Called whenever a JWT is created or updated
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id
      }
      if (account) {
        token.accessToken = account.access_token
      }
      return token
    },

    // Called whenever a session is checked
    async session({ session, token }) {
      session.user.id = token.id as string
      session.accessToken = token.accessToken as string
      return session
    },

    // Called on middleware and server-side auth checks
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = request.nextUrl.pathname.startsWith("/dashboard")
      
      if (isOnDashboard) {
        return isLoggedIn
      }
      
      return true
    },

    // Called when user is redirected
    async redirect({ url, baseUrl }) {
      // Allows relative callback URLs
      if (url.startsWith("/")) return `${baseUrl}${url}`
      // Allows callback URLs on the same origin
      else if (new URL(url).origin === baseUrl) return url
      return baseUrl
    },
  },
})

数据库适配器(可选)

如需在数据库中持久化用户、账户和会话,请安装 Prisma 适配器:

pnpm add @auth/prisma-adapter

然后在 auth.ts 中配置:

import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: "database",
  },
  providers: [
    // ... providers
  ],
})

必需的 Prisma 架构:

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

API 路由

自定义 API 端点
// app/api/user/route.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export async function GET() {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }

  return NextResponse.json({
    user: session.user,
  })
}
受保护路由辅助函数
// lib/auth-helpers.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
import type { Session } from "next-auth"

export async function withAuth(
  handler: (session: Session) => Promise<NextResponse>
) {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }

  return handler(session)
}

// Usage
export async function GET() {
  return withAuth(async (session) => {
    return NextResponse.json({ userId: session.user.id })
  })
}

最佳实践

安全性
  • 始终对密码进行哈希处理:使用 bcrypt、argon2 或类似工具
  • 在生产环境中使用 HTTPS:安全传输 Cookie 的必要条件
  • 验证环境变量:检查 AUTH_SECRET 和 Provider 凭据
  • 设置安全的 Cookie 选项:
    cookies: {
      sessionToken: {
        name: `__Secure-next-auth.session-token`,
        options: {
          httpOnly: true,
          sameSite: 'lax',
          path: '/',
          secure: process.env.NODE_ENV === 'production',
        },
      },
    }
    
  • 实施速率限制:保护登录端点
  • 使用 CSRF 防护:v5 默认启用
  • 验证重定向:使用 redirect 回调防止开放重定向
会话管理
  • 使用适当的 maxAge:默认为 30 天,请根据安全要求进行调整
  • 定期更新会话:使用 updateAge 刷新会话数据
  • 妥善处理会话过期:提供清晰的 UI 提示
  • 安全存储会话:对敏感应用使用数据库策略
提供商配置
  • Google OAuth:仅请求所需的最少权限范围
  • 凭据:始终使用 zod 或类似工具验证输入
  • 多个提供商:谨慎允许账户关联
  • 提供商专属逻辑:使用回调处理不同提供商之间的差异
性能
  • 缓存会话检查:使用中间件保护路由
  • 尽量减少数据库调用:适当情况下使用 JWT 策略
  • 优化数据库查询:为频繁查询的字段添加索引
  • 使用 Edge Runtime:加快中间件中的身份验证检查
类型安全
  • 正确扩展类型:使用模块增强添加自定义会话字段
  • 验证输入:使用 zod 进行运行时类型检查
  • TypeScript 严格模式:启用以提高类型安全性

常见模式

使用中间件保护页面
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const { pathname } = req.nextUrl

  // Public routes
  const publicRoutes = ['/auth/signin', '/auth/register', '/']
  if (publicRoutes.includes(pathname)) {
    return NextResponse.next()
  }

  // Protected routes
  if (!isLoggedIn) {
    const signInUrl = new URL('/auth/signin', req.url)
    signInUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(signInUrl)
  }

  // Role-based access
  const adminRoutes = ['/admin']
  if (adminRoutes.some(route => pathname.startsWith(route))) {
    if (req.auth.user.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  return NextResponse.next()
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
多提供商设置
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      // ... credentials config
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile }) {
      // Link accounts with same email
      if (account?.provider !== "credentials") {
        const existingUser = await prisma.user.findUnique({
          where: { email: user.email },
        })
        
        if (existingUser) {
          // Link account to existing user
          await prisma.account.create({
            data: {
              userId: existingUser.id,
              type: account.type,
              provider: account.provider,
              providerAccountId: account.providerAccountId,
              access_token: account.access_token,
              refresh_token: account.refresh_token,
            },
          })
        }
      }
      return true
    },
  },
})
自定义登录页面
// app/auth/signin/page.tsx
import { signIn } from "@/auth"
import { redirect } from "next/navigation"

export default function SignInPage({
  searchParams,
}: {
  searchParams: { callbackUrl?: string }
}) {
  const callbackUrl = searchParams.callbackUrl || "/dashboard"

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-8 p-8">
        <h1 className="text-2xl font-bold text-center">Sign In</h1>
        
        {/* OAuth Providers */}
        <div className="space-y-4">
          <form
            action={async () => {
              "use server"
              await signIn("google", { redirectTo: callbackUrl })
            }}
          >
            <button 
              type="submit"
              className="w-full bg-white border border-gray-300 text-gray-700 py-2 px-4 rounded hover:bg-gray-50"
            >
              Continue with Google
            </button>
          </form>
        </div>

        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <div className="w-full border-t border-gray-300" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="bg-white px-2 text-gray-500">Or</span>
          </div>
        </div>

        {/* Credentials Form */}
        <form
          action={async (formData) => {
            "use server"
            try {
              await signIn("credentials", {
                email: formData.get("email"),
                password: formData.get("password"),
                redirectTo: callbackUrl,
              })
            } catch (error) {
              redirect(`/auth/signin?error=CredentialsSignin&callbackUrl=${callbackUrl}`)
            }
          }}
          className="space-y-4"
        >
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Password
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <button
            type="submit"
            className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
          >
            Sign In
          </button>
        </form>
      </div>
    </div>
  )
}
基于角色的访问控制(RBAC)
// lib/auth-rbac.ts
import { auth } from "@/auth"

export type Role = "admin" | "user" | "guest"

export async function checkRole(allowedRoles: Role[]) {
  const session = await auth()
  
  if (!session?.user) {
    return false
  }

  const userRole = session.user.role as Role
  return allowedRoles.includes(userRole)
}

// Usage in Server Component
export default async function AdminPage() {
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    redirect("/unauthorized")
  }

  return <div>Admin Dashboard</div>
}

// Usage in Server Action
export async function deleteUser(userId: string) {
  "use server"
  
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    throw new Error("Unauthorized")
  }

  const { prisma } = await import("@/lib/prisma")
  await prisma.user.delete({ where: { id: userId } })
}

从 v4 迁移到 v5

主要差异
  1. 导入变更:next-auth 包保持不变,但导入方式已简化
  2. 通用 auth():使用 auth() 替换 getServerSession
  3. 中间件:直接将 auth 用作中间件
  4. 配置:配置更加精简,所需选项更少
迁移步骤
// v4 (old)
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function GET() {
  const session = await getServerSession(authOptions)
}

// v5 (new)
import { auth } from "@/auth"

export async function GET() {
  const session = await auth()
}
// v4 middleware (old)
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token }) => !!token,
  },
})

// v5 middleware (new)
export { auth as middleware } from "@/auth"

故障排除

常见问题

未设置 AUTH_SECRET:

Error: AUTH_SECRET environment variable is not set

在 .env.local 中生成并设置 AUTH_SECRET

Google OAuth 重定向不匹配:

Error: redirect_uri_mismatch

确保 Google Console 中的重定向 URI 与此一致:http://localhost:3000/api/auth/callback/google

会话未持久化:

  • 检查是否正确设置了 AUTH_URL
  • 确认 Cookie 未被阻止
  • 确保已设置 sessionToken Cookie(检查浏览器 DevTools)

会话相关的 TypeScript 错误:

  • 使用模块增强扩展 Session 和 JWT 类型
  • 运行 pnpm tsc --noEmit 检查类型错误

Credentials 提供商无法正常工作:

  • 确保 session.strategy 设置为 "jwt"
  • 检查 authorize 函数是否返回包含 id 字段的正确用户对象
  • 验证密码哈希与比较逻辑

资源

评论

支持 Markdown