从Next-Auth到Prisma,用最新潮的技术栈做登录

🧑‍💻
推荐全栈学习资源:
  • Next.js 中文文档:样式和官网一样的中文文档,创造沉浸式Next.js中文学习体验。
  • 《Chrome插件全栈开发》:真实出海项目的实战教学课,讲解Chrome插件和Next.js端的全栈开发,帮助你半个月内成为全栈出海工程师。
  • 引言

    使用React框架的前端工程师应该都听过一句话:React的成员不是在NextJS就是在去NextJS的路上。NextJS的核心团队多来自React,这正是NextJS快速成长的原因之一。

    那么NextJS有什么特性呢?NextJSReact应用提供了非常便利的开箱即用功能,如路由、页面预获取、服务器端渲染等,极大地简化和加速了开发,这也让NextJS成为前端领域的当红炸子鸡。

    NextJS背后的公司是Vercel,这家公司还有当前最热门的自动化部署云平台Vercel

    NextJS社区里,还有下一代Node.jsTypeScriptORM——Prisma;还有英国开发者Iain Collins开发了next-auth,这是一个支持多种登录方式如 OAuth、email、credentials的库,能够极大简化我们开发登录功能的时间。

    NextJS + Next-Auth + Prisma,随着三个主角悉数亮相,就可以明确本文的目标了:用NextJS、Next-Auth、Prisma来完成一个Github OAuth登录的功能。

    看完本文你将学到:

    • 创建你的Github应用
    • NextJS项目中使用next-auth完成登录流程(不限于github登录)
    • docker构建开发环境postgres数据库
    • 当前最热门的orm——prisma的基本使用

    创建Github应用

    本文假定你对OAuth有基本的了解,如果你还不了解,找个可以扫码登录WeChat的网站体验一下,那就叫OAuth授权登录。

    现在我们在自己的Github后台创建一个应用,用户通过OAuth授权登录,那就成为你这个应用的用户啦。

    第一步:到 https://github.com/settings/apps 创建OAuth应用

    第二步:填写应用信息

    github oauth register.png

    注意:Authorization callback URL是授权登录后的回调地址,如果登录流程是自己开发,你可以根据自己代码来填,但是本文是用next-auth,所以得按next-auth的规范来做,必须填/api/auth/callback/github

    第三步:创建完成后,会进入应用后台,此时需要生成Clientsecrets,并保存下Client IDClient secrets

    github oauth register2.png

    因为现在还处于本地调试阶段,所以我把设置的两个URL改成localhost了:

    github oauth register3.png

    Github应用创建就是这么简单,很适合用来做一些实验性的功能。

    扩展阅读:

    如果想学习GoogleOAuth,请猛烈点击我的历史文章:

    谷歌OAuth2.0开发的正确配置步骤

    用Next-Auth实现登录

    先创建一个Next项目:

    npx create-next-app@latest

    安装next-auth

    yarn add next-auth

    nextjs v13.2推出后,next-auth已支持app router模式下在app文件夹内构建API,但是鉴于官方文档主要使用方式仍然是放在pages文件夹下,所以本例也将在pages下进行API编写。

    pages/api/auth中创建一个名为[...nextauth].ts的文件

    import NextAuth from "next-auth"
     
    import { authOptions } from "@/lib/auth"
     
    // @see ./lib/auth
    export default NextAuth(authOptions)

    pages/api/auth/[...nextauth].ts中使用[...nextauth]是为了动态匹配nextauth的所有API路由,如:

    • /api/auth/callback 处理认证回调
    • /api/auth/signin 处理登录
    • /api/auth/signout 处理登出
    • /api/auth/session 获取session等等

    也就是说,使用[...nextauth]可以动态匹配所有包含/api/auth/nextauthAPI路由。

    我们把next-auth的基本配置放在了lib/auth.ts里:

    import NextAuth, { NextAuthOptions } from "next-auth"
    import GithubProvider from 'next-auth/providers/github';
     
    export const authOptions: NextAuthOptions = {
      session: {
        strategy: "jwt",
      },
      pages: {
        signIn: "/auth/login",
        signOut: '/auth/logout',
      },
      providers: [
        GithubProvider({
          clientId: `${process.env.GITHUB_ID}`,
          clientSecret: `${process.env.GITHUB_SECRET}`,
          httpOptions: {
            timeout: 50000,
          },
        }),
        // GoogleProvider({
        //   clientId: process.env.GOOGLE_ID,
        //   clientSecret: process.env.GOOGLE_SECRET
        // }),
      ],
      callbacks: {
        session: async ({ session, token }) => {
          return token
        }
      },
    }
     
    export default NextAuth(authOptions)

    next-auth的服务端内容就是这些,乍一看容易一头雾水,但确实就是这么简单,它把中间繁琐的过程都封装起来了。

    现在来写个这样的登录页

    github oauth button.png

    核心代码是「使用Github登录」的按钮

    // components/UserAuthForm.tsx
     
    "use client";
     
    import * as React from "react";
    import { signIn } from "next-auth/react";
     
    import { cn } from "@/lib/utils";
    import { buttonVariants } from "@/components/ui/button";
    import { Icons } from "@/components/Icons";
     
    interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
     
    export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
      const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false);
     
      const login = async () => {
        setIsGitHubLoading(true);
        signIn("github", { // 登录方法,第一个参数标注平台
          callbackUrl: `${window.location.origin}`, // 设置登录成功后的回调地址
        });
      };
     
      return (
        <div className={cn("grid gap-6", className)} {...props}>
          <button
            type="button"
            className={cn(buttonVariants())}
            onClick={login}
            disabled={isGitHubLoading}
          >
            {isGitHubLoading ? (
              <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
            ) : (
              <Icons.gitHub className="mr-2 h-4 w-4" />
            )}{" "}
            Github
          </button>
        </div>
      );
    }

    前端部分也完成了。

    这时候还要配一个环境变量

    # .env
    GITHUB_ID=YOUR_GITHUB_ID
    GITHUB_SECRET=YOUR_GITHUB_SECRET
     
    # NEXTAUTH_SECRET是必填项,用命令生成: openssl rand -base64 32
    NEXTAUTH_SECRET=YOUR_NEXTAUTH_SECRET
    NEXTAUTH_URL=http://localhost:3001 # 告诉next-auth授权回调的基础 URL,这个环境变量是必须的,虽然它没有在我们的代码里体现

    现在试一下能不能完成Github OAuth登录。点击按钮确实会跳到授权页

    github oauth page.png

    授权后会跳到首页,完成了授权登录流程了。

    需要一个更准确的信息证明真的完成授权登录了?那就在首页把个人信息回显出来。这依然需要用到next-authapi。让我们在lib下面新建一个文件叫做session.ts

    // session.ts
    import { getServerSession } from "next-auth/next"
     
    import { authOptions } from "@/lib/auth"
     
    export async function getCurrentUser() {
      const session = await getServerSession(authOptions)
     
      return session?.user
    }

    app/page.tsx调用

    import Image from 'next/image'
    import { getCurrentUser } from "@/lib/session";
     
    export default async function Home() {
      const user = await getCurrentUser();
      console.log(user);
     
    	return (
    		<main>
    			……<div>
            <div className="flex">
              {user?.image ? (
                <>
                  {" "}
                  Current User:{" "}
                  <Image
                    className="relative rounded-full ml-3 dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
                    src={user.image}
                    alt="Next.js Logo"
                    width={36}
                    height={36}
                    priority
                  />
                </>
              ) : (
                <></>
              )}
            </div>
            {!user ? (
              <div className="">
                Next-Auth的demo请到
                <Link
                  href="/login"
                  className="hover:text-brand underline underline-offset-4"
                >
                  登录页
                </Link>
                体验
              </div>
            ) : (
              <SignOut></SignOut>
            )}
          </div>
    			……
    		</main>
    	)
    }

    为了Image可以生效,我们需要在next.config.js里添加可信域

    /** @type {import('next').NextConfig} */
    const nextConfig = {
      images: {
        domains: ['avatars.githubusercontent.com'], // 添加github头像服务的域名
      },
    }
     
    module.exports = nextConfig

    现在在首页就可以看到用户信息回显了

    6、github oauth success.png

    如果你自己开发过oauth登录,再看到next-auth的流程,你一定会很兴奋,因为next-auth帮我们处理掉了很多中间过程。如果你没开发过oauth,可以到这一篇看看自己开发oauth登录有多麻烦:接入谷歌OAuth2.0登录的分析和代码实践

    搭建postgres测试数据库

    有时候我们不止是需要第三方授权的用户信息,我们还想自己保存一个用户表,把用户基本信息和我们自定义的一些字段共存,那么我们就需要建一个数据库了。

    Mysqloracle收购后,postgres成为开源社区里最闪亮的新星。postgres也是本文代码用到的数据库。

    如果我们自己安装postgres,很多操作会显得非常繁琐,但如果在docker中安装,一切就变得非常丝滑。

    如果你还没安装docker,请通过官网安装一下,安装后用命令验证是否安装成功,如果安装成功,会返回docker的版本号。

    docker-compose -v

    docker安装完成后,到项目根目录下创建文件docker-compose.yml

    # docker-compose.yml
    version: '3.1'
    services:
      nextjs-learn-domes:
        image: postgres
        volumes:
          - ./postgres:/var/lib/postgresql/data
        ports:
          - 5432:5432
        environment:
          - POSTGRES_USER=myuser
          - POSTGRES_PASSWORD=mypassword
     
      adminer:
        image: adminer
        restart: always
        ports:
          - 8080:8080

    启动docker

    docker-compose up -d

    如果启动失败,请排查5432和8080端口是否被占用,如果被占用需要先关闭占用的服务再启动docker

    现在访问http://localhost:8080就可以登录数据库了

    7、db login.png

    如果要关闭docker,可以执行命令

    docker-compose down

    如果你想学习Docker的基本知识,请阅读文章前端有了Docker,全栈之路更轻松了

    用Prisma操作数据库

    Prisma是下一代Node.jsTypeScriptORM。在Node中用过Sequelize操作数据库的兄弟都知道,这是一个让你可以用写对象的方式来写sql的工具,极大简化了前端对sql的学习成本和开发成本,Prisma也是这样的工具。

    这里有对PrismaSequelize的简单优势对比:

    1. Prisma拥有自己的查询语言Prisma Query Language(PQL),语法更接近SQL,上手更简单。Sequelize使用的是JS语法,需要学习更多API。
    2. Prisma有自动生成并更新数据库Schema的功能,可以通过Prisma Client直接访问数据库,更符合现代开发方式。Sequelize需要更多手动配置。
    3. Prisma基于下一代ORM理念,如无缝的关系映射、类型安全等特性。Sequelize相对更传统。
    4. Prisma有更好的TypeScript支持,提供良好的类型推导。SequelizeTypeScript体验较差。
    5. Prisma有直观的数据模型可视化功能。Sequelize需要通过代码理解数据结构。
    6. Sequelize社区更加成熟,资源更丰富。Prisma作为较新框架,资源相对较少。

    开始使用Prisma

    yarn add prisma @prisma/client

    初始化Prisma

    npx prisma init

    初始化后,在根目录会出现一个prisma的文件夹,这个文件夹用来存放Prisma相关配置;里面还有一个schema.prisma文件,是核心的数据库Schema定义文件,开发时主要通过修改它来更新数据库模型设计。

    再去看.env文件,会发现多了一条配置

    DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

    需要改成我们的postgres配置

    DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"

    创建数据库

    现在开始思考我们的用户表结构,未来我们希望通过next-auth接入多个第三方平台的OAuth,那么可以记录下用户在第三方平台的唯一id,还需要抹平不同平台的用户信息字段。那么就可以在用户表里添加subplatform,再统一用户信息字段usernameavatar

    schema.prisma是唯一的Schema定义文件,定义User表结构如下:

    // schema.prisma
     
    ……
     
    model User {
      id        Int      @id @default(autoincrement())
      sub       String   @unique // 第三方平台的唯一id
      platform  String   // 第三方平台标识,如:github google
      username  String
      avatar    String
      email     String
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }

    定义了表结构后,本身不会自动创建表,需要执行migrate命令才会由Prisma来生成和执行创建表的SQL语句

    npx prisma migrate dev --name "init"

    打开数据库,会发现现在User表已经创建出来了

    8、db generate success.png

    如果你想深入了解一下prisma migrate命令,请猛烈点击:prisma migrate命令简明教程

    实例化PrismaClient

    创建lib/prisma.ts,实例化PrismaClient

    import { PrismaClient } from "@prisma/client";
     
    declare global {
      // eslint-disable-next-line no-var
      var prisma: PrismaClient
    }
     
    let prisma: PrismaClient;
     
    if (process.env.NODE_ENV === "production") {
      prisma = new PrismaClient();
    } else {
      if (!global.prisma) {
        global.prisma = new PrismaClient();
      }
      prisma = global.prisma;
    }
    export default prisma;

    PrismaClient实例的作用是连接数据库并执行数据库操作,可以把它理解为一个数据库客户端,可以通过它来发送数据库查询、修改数据。

    修改auth/ts,我们尝试把从Github获取的用户信息存到User表里

    ……
    import prisma from "@/lib/prisma";
    import { UserInfo } from "@/types/user";
     
    ……
     
    callbacks: {
        session: async ({ session, token }) => {
          const res = await prisma.user.upsert({
            where: {
              sub: token.sub
            },
            update: {
              // 使用token中的数据
              username: token.name,
              avatar: token.picture,
              email: token.email
            },
            create: {
              // 使用token中的数据 
              sub: token.sub,
              username: token.name,
              avatar: token.picture,
              email: token.email,
              platform: 'github',
            }
          })
          if (res) {
            session.user = {
              sub: res.sub,
              platform: res.platform,
              username: res.username,
              avatar: res.avatar,
              email: res.email,
            } as UserInfo
          }
          return session
        }
      },
     
    ……
     

    回到页面,重新登录一下,会发现原本显示头像的位置不显示了,说明已经拿到我们封装后的用户信息了,只要修改一下字段就可以回显。

    结语

    通过本文的学习,我们用全新的技术栈NextJS+Next-Auth+Postgres+Prisma完成了一个登录模块,如果你自己折腾过登录流程,一定能感受到Next-Auth的强大。

    本文涉及的技术栈,都是当前海外前端圈流行的新技术,比较适合个人开发者快速开发创意原型或实用工具。这套技术组合both前后端都能快速高效地工作,是构建web应用的绝佳选择。

    源码与演示

    源码:👉NextAuth-Prisma

    在线演示:👉NextAuth登录

    专栏资源

    专栏介绍:以实战的角度进行Next.js生态圈的技术栈分享,内容包括但不限于:Next.js理论知识、功能模块设计思路、实战中使用到的技术栈。这是一个长期更新的专栏,我会持续把自己的思考和经验提炼分享出来,欢迎关注我的专栏👇

    专栏地址:👉Next.js生态圈实战

    专栏演示站:👉Next.js Demos

    专栏源码仓库:👉Github - Source Code

    交个朋友:👉加入「独立全栈交流群」