从Next-Auth到Prisma,用最新潮的技术栈做登录
引言
使用React
框架的前端工程师应该都听过一句话:React
的成员不是在NextJS
就是在去NextJS
的路上。NextJS
的核心团队多来自React
,这正是NextJS
快速成长的原因之一。
那么NextJS
有什么特性呢?NextJS
为React
应用提供了非常便利的开箱即用功能,如路由、页面预获取、服务器端渲染等,极大地简化和加速了开发,这也让NextJS
成为前端领域的当红炸子鸡。
NextJS
背后的公司是Vercel
,这家公司还有当前最热门的自动化部署云平台Vercel
。
在NextJS
社区里,还有下一代Node.js
和TypeScript
的ORM
——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
应用
第二步:填写应用信息
注意:Authorization callback URL
是授权登录后的回调地址,如果登录流程是自己开发,你可以根据自己代码来填,但是本文是用next-auth
,所以得按next-auth
的规范来做,必须填/api/auth/callback/github
第三步:创建完成后,会进入应用后台,此时需要生成Client和secrets,并保存下Client ID和Client secrets。
因为现在还处于本地调试阶段,所以我把设置的两个URL改成localhost
了:
Github
应用创建就是这么简单,很适合用来做一些实验性的功能。
扩展阅读:
如果想学习Google
的OAuth
,请猛烈点击我的历史文章:
用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/
和nextauth
的API
路由。
我们把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登录」的按钮
// 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
登录。点击按钮确实会跳到授权页
授权后会跳到首页,完成了授权登录流程了。
需要一个更准确的信息证明真的完成授权登录了?那就在首页把个人信息回显出来。这依然需要用到next-auth
的api
。让我们在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
现在在首页就可以看到用户信息回显了
如果你自己开发过oauth
登录,再看到next-auth
的流程,你一定会很兴奋,因为next-auth
帮我们处理掉了很多中间过程。如果你没开发过oauth
,可以到这一篇看看自己开发oauth
登录有多麻烦:接入谷歌OAuth2.0登录的分析和代码实践
搭建postgres测试数据库
有时候我们不止是需要第三方授权的用户信息,我们还想自己保存一个用户表,把用户基本信息和我们自定义的一些字段共存,那么我们就需要建一个数据库了。
在Mysql
被oracle
收购后,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
就可以登录数据库了
如果要关闭docker
,可以执行命令
docker-compose down
如果你想学习Docker
的基本知识,请阅读文章:前端有了Docker,全栈之路更轻松了
用Prisma操作数据库
Prisma是下一代Node.js
和TypeScript
的ORM
。在Node中用过Sequelize
操作数据库的兄弟都知道,这是一个让你可以用写对象的方式来写sql
的工具,极大简化了前端对sql
的学习成本和开发成本,Prisma
也是这样的工具。
这里有对
Prisma
和Sequelize
的简单优势对比:
Prisma
拥有自己的查询语言Prisma Query Language(PQL)
,语法更接近SQL,上手更简单。Sequelize
使用的是JS语法,需要学习更多API。Prisma
有自动生成并更新数据库Schema
的功能,可以通过Prisma Client
直接访问数据库,更符合现代开发方式。Sequelize
需要更多手动配置。Prisma
基于下一代ORM
理念,如无缝的关系映射、类型安全等特性。Sequelize
相对更传统。Prisma
有更好的TypeScript
支持,提供良好的类型推导。Sequelize
的TypeScript
体验较差。Prisma
有直观的数据模型可视化功能。Sequelize
需要通过代码理解数据结构。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,还需要抹平不同平台的用户信息字段。那么就可以在用户表里添加sub
和platform
,再统一用户信息字段username
、avatar
。
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
表已经创建出来了
如果你想深入了解一下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
交个朋友:👉加入「独立全栈交流群」