Nextjs应用安全是用户体验的关键一步。下面我们用 Next.js 与 MongoDB 实现用户身份验证、登录和注册功能的过程。
文中的Nextjs使用最新的App Router路由,需要安装一些基本的库
npm i bcryptjs jsonwebtoken mongoose
第一步、数据库链接
1、要将我们的应用程序连接到 MongoDB 数据库,请在 MongoDB Atlas 中创建一个帐户。
2、然后复制连接字符串,然后将其粘贴到 .env 文件中
添加图片注释,不超过 140 字(可选)
如果对相关的过程不太了解的小伙伴,可以看这篇文章:
https://juejin.cn/post/7368385859018113051
第二步、编写名为 connect 的异步函数,连接MongoDB 数据库。
// 文件路径 /src/connect.ts
import mongoose from 'mongoose';
export async function connect() {
try{
mongoose.connect(process.env.MONGO_URI!);
const connection = mongoose.connection;
connection.on('connected', () => {
console.log("MongoDB connected successfully');
})
connection.on('error', (err) => {
console.log('MongoDB connection error' + err);
process.exit();
})
} catch (error) {
console.log(error);
}
}
第三步、用户注册实现
客户端页面
// 文件路径 /src/app/signup/page.tsx
'use client';
import { Card, Form, Button, Input } from 'antd';
import { useRouter } from 'next/navigation';
function SignupPage() {
const nav = useRouter();
const handleFinish = async (v:any) => {
const res = await fetch('/api/admin/signup', {
method: 'POST',
body: JSON.stringify(v),
})
const data = await res.json();
if (data.success) {
nav.push('/login');
}
}
return (
<div className='login-form pt-20'>
<Card title='注册' className='w-8/12 !mx-auto'>
<Form
labelCol={{ span: 3 }}
onFinish={handleFinish}
>
<Form.Item name='username' label='用户名'>
<Input placeholder='请输入用户名' />
</Form.Item>
<Form.Item name='password' label='密码'>
<Input placeholder='请输入密码' />
</Form.Item>
<Form.Item>
<Button block type='primary' htmlType='submit'>
注册
</Button>
</Form.Item>
</Form>
</Card>
</div>
);
}
export default SignupPage;
用户数据向“/api/admin/signup”发出 POST 请求,当服务端返回success后页面跳转到 login页面。
创建用户模型
将用户数据存储在 MongoDB 中,我们将首先创建一个用户模型。
// 文件路径 /src/models/user.js
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
userName: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: new Date(),
},
updatedAt: {
type: Date,
default: new Date(),
},
});
userSchema.pre("save", function (next) {
if (this.isNew) {
this.createdAt = this.updatedAt = new Date();
} else {
this.updatedAt = new Date();
}
next();
})
const User = mongoose.models.users || mongoose.model('users', userSchema);
export default User;
因为每次都与MongoDB连接,我们使用这种语法 const User = mongoose.models.users || mongoose.model('users', userSchema);
服务端
// 文件路径 /src/api/users/signup/route.ts
import { connect } from "@/db";
import User from "@/models/user";
import { NextRequest, NextResponse } from "next/server";
import bcryptjs from "bcryptjs";
// 数据库连接
connect();
export const POST = async (request: NextRequest) => {
try {
// 获取请求体
const req = await request.json();
const { userName, password } = req;
// 查询数据库 是否存在该用户
const user = await User.findOne({ userName });
// 如果存在该用户
if (user) {
return NextResponse.json({
success: false,
errorMessage: '用户名已存在',
}, {status: 400});
}
// 密码加密
const salt = await bcryptjs.genSalt(10);
const hash = await bcryptjs.hash(password, salt);
// 创建用户
const newUser = new User({
userName,
password: hash,
});
// 保存用户
const res = await newUser.save();
// 返回响应
return NextResponse.json({
success: true,
errorMessage: '注册成功',
});
} catch (error) {
return NextResponse.json({
success: false,
errorMessage: '注册失败',
}, {status: 500});
}
};
第四步、用户登录
客户端
// 文件路径 /src/app/login/page.tsx
'use client';
import { Card, Form, Button, Input } from 'antd';
import { useRouter } from 'next/navigation';
function LoginPage() {
const nav = useRouter();
return (
<div className='login-form pt-20'>
<Card title='Next全栈管理后台' className='w-4/5 !mx-auto'>
<Form
labelCol={{ span: 3 }}
onFinish={async (v) => {
const res = await fetch('/api/admin/login', {
method: 'POST',
body: JSON.stringify(v),
}).then((res) => res.json());
console.log(res);
nav.push('/admin/dashboard');
}}
>
<Form.Item name='userName' label='用户名'>
<Input placeholder='请输入用户名' />
</Form.Item>
<Form.Item name='password' label='密码'>
<Input.Password placeholder='请输入密码' />
</Form.Item>
<Form.Item>
<Button block type='primary' htmlType='submit'>
登陆
</Button>
</Form.Item>
</Form>
<a href='/signup'>注册</a>
</Card>
</div>
);
}
export default LoginPage;
服务端
// 文件路径 /src/api/admin/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import {connect} from '@/db';
import User from '@/models/user';
import bcryptjs from 'bcryptjs';
import jwt from 'jsonwebtoken';
// 数据库连接
connect();
export const POST = async (request: NextRequest) => {
try {
// 获取请求体
const req = await request.json();
const { userName, password } = req;
// 查询数据库 是否存在该用户
const user = await User.findOne({ userName });
// 如果不存在该用户
if (!user) {
return NextResponse.json(
{
success: false,
errorMessage: '用户名不存在',
},
{ status: 400 }
);
}
// 密码比对
const isMatch = await bcryptjs.compare(password, user.password);
if (!isMatch) {
return NextResponse.json(
{
success: false,
errorMessage: '登陆失败',
},
{ status: 400 }
);
}
const tokenData = {
id: user._id,
userName: user.userName,
}
const token = jwt.sign(tokenData, process.env.JWT_SECRET!, {expiresIn: '1d'});
const response = NextResponse.json(
{
success: true,
errorMessage: '登陆成功',
}
);
response.cookies.set('token', token, {httpOnly: true});
return response;
} catch (error) {
return NextResponse.json(
{
success: false,
errorMessage: '登陆失败',
},
{ status: 500 }
);
}
};
第五、客户端路由中间件
在客户端页面切换到哦时候,如果没有cookie自动跳转到登录页面。
// 文件路径 src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
// console.log('中间件执行了');
if (request.nextUrl.pathname.startsWith('/')) {
// 访问的如果是管理后台,我们可以在这里做判断
if (!request.nextUrl.pathname.startsWith('/login' || '/signup')) {
// 不是登陆页面的时候,判断是否登陆过
if (request.cookies.get('token')) {
// 已经登陆了,什么都不做
} else {
// 跳转到登陆页面
return NextResponse.redirect(new URL('/login', request.url));
}
}
}
}
第六、用户退出
客户端
const logout = async () => {
await fetch('/api/admin/logout', {
method: 'POST',
}).then(() => {
nav.push('/login');
});
}
服务端
// 文件路径 /src/api/admin/logout/route.ts
import { NextRequest, NextResponse } from 'next/server';
export const POST = async (request: NextRequest) => {
try {
const response = NextResponse.json({
success: true,
errorMessage: '退出',
});
response.cookies.set('token', '', {httpOnly: true, expires: new Date(0)});
return response;
} catch (error) {
return NextResponse.json({
success: false,
errorMessage: error,
}, {status: 500});
}
};
在服务端清除用户的令牌。
- httpOnly: true 选项确保 cookie 只能在服务器端访问,而不能通过客户端 JavaScript 访问,从而增强安全性。
- expires: new Date(0) 选项将 cookie 的过期日期设置为过去的日期,有效地将其删除。
第七、服务端接口中间件,对用户进行鉴权
在访问服务端接口的时候,需要判断用户的token是否已经过期,来对用户进行鉴权。
// 文件路径 /src/helper/getDataFromToken.ts
import jwt from 'jsonwebtoken';
import { NextRequest } from 'next/server';
export function getDataFromToken(req: NextRequest) {
try {
// 获取token
const token = req.cookies.get('token')?.value || '';
// 解析token
const decodedToken:any = jwt.verify(token, process.env.JWT_SECRET!);
if (decodedToken) {
// 返回用户id
return decodedToken.id
} else {
return null;
}
} catch (error) {
return null;
}
}
在本文中,我们介绍了 Next.js 的基础知识,更加复杂的用户登录和鉴权需要大家根据具体项目实际情况来构建。感谢大家!