掘金 后端 ( ) • 2024-05-16 17:20

Nextjs应用安全是用户体验的关键一步。下面我们用 Next.js 与 MongoDB 实现用户身份验证、登录和注册功能的过程。

文中的Nextjs使用最新的App Router路由,需要安装一些基本的库

npm i bcryptjs jsonwebtoken mongoose

第一步、数据库链接

1、要将我们的应用程序连接到 MongoDB 数据库,请在 MongoDB Atlas 中创建一个帐户。

2、然后复制连接字符串,然后将其粘贴到 .env 文件中

image.png

添加图片注释,不超过 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 的基础知识,更加复杂的用户登录和鉴权需要大家根据具体项目实际情况来构建。感谢大家!