Nuxt3接口安全实战:JWT鉴权+中间件拦截|2025最新避坑指南

昨天我们实现了 nuxt 链接数据库并编写了简单接口,但是有一个问题就是这个接口任何人在不登陆的情况下都可以进行访问。今天就来给数据加把锁 🔒

对于接口数据的保护有很多方法,这里我们使用 JWT(全称 JSON Web Token),她的作用简单来说就是用来传递信息,并且信息是加密的,所以只有经过验证的才能访问到数据。

简单工作流程:

1-用户登录 → 服务端生成 Token → 返回给前端

2-前端请求接口时在 Header 携带 Token

3-服务端验证签名有效性 → 放行合法请求

🔐 安全提示:Payload 仅存储用户 ID 等非敏感信息,切勿存放密码!

1-安装依赖包:

npm i jsonwebtoken

2-生成 Token

这里因为我们使用的 nuxt,所以我会编写一个简单的注册接口,首先我们需要在 server 目录下创建 api 文件夹,在 api 目录中创建 login 目录,最后创建 index.ts文件(当然你也可以创建js文件),最终的目录结构应该是server/api/login/index.ts。代码如下:

import { defineEventHandler, readBody } from "h3";
import UserList from "./userModel";
import jwt from "jsonwebtoken";
import { config } from "dotenv";
config();
const createToken = (payload: string | Buffer | object) => {
  return jwt.sign(payload, process.env.JWT_SECRET as string, {
    expiresIn: 7200,
  });
};
export default defineEventHandler(async (event) => {
  try {
    if (!process.env.JWT_SECRET) {
      return { status: 500, message: "JWT_SECRET错误" };
    }
    const body = await readBody(event);
    if (!body.account || !body.password) {
      return {
        status: 400,
        message: "参数缺失",
      };
    }
    const { account, password } = body;
    // 查找用户是否存在
    const user = await UserList.findOne({ account });

    // 走登录流程
    if (user && user.password === password) {
      const token = createToken({ account, id: user["_id"] });
      setCookie(event, "auth_token", token, {
        httpOnly: true, // ✨ 禁止客户端 JS 访问(防 XSS)
        maxAge: 10, // ⏳ 有效期秒数(比 expires 更推荐)
        path: "/", // 🗺️ Cookie 生效路径
        sameSite: "lax", // 🔒 默认值(防御 CSRF)
      });
      return { status: 200, message: "登录成功", };
    } else if (user && user.password !== password) {
      return { status: 400, message: "账号密码错误" };
    }
    // 创建用户
    const newUser = new UserList({ account, password });
    await newUser.save();
    const token = createToken({ account, id: newUser["_id"] });
    setCookie(event, "auth_token", token, {
      httpOnly: true, // ✨ 禁止客户端 JS 访问(防 XSS)
      maxAge: 60 * 60 * 2, // ⏳ 有效期秒数(比 expires 更推荐)
      path: "/", // 🗺️ Cookie 生效路径
      sameSite: "lax", // 🔒 默认值(防御 CSRF)
    });
    return { status: 200, message: "登录成功,新用户", };
  } catch (err) {
    throw createError({
      statusCode: 500,
      message: "服务器错误",
    });
  }
});

这里需要注意jwt.sign中的expiresIn应该是数字或者官方支持的字符串,此错编写的7200就是2小时,如果要设置其他时间,可以参考官方文档。

❗这里有一个坑需要注意,如果expiresIn是保存在了env配置文件中那么需要注意这个参数是数字或者是可以转换为数字的字符串,否则会报错。

这里我们可以看到我们没有返回token字段给前端,而是使用了setCookie()方法,这个方法可以设置cookie,并且可以设置过期时间,并且可以设置其他参数,比如path,domain,secure,httpOnly等。当然这里需要注意如果你是前后端分离的写法这里就需要在返回结构处返回token,比如说你后端使用express等框架编写。

3-使用中间件验证Token

我们验证Token需要编写中间件,中间件是会自动执行的我们不需要单独的去启动,接下来先创建文件,我们在server目录下创建middleware目录,并创建verifyToken.ts文件,代码如下:

import jwt from "jsonwebtoken";
import { defineEventHandler, getCookie } from "h3";
import { config } from "dotenv";
config();
export default defineEventHandler(async (event) => {
  // 仅在 /api 路由下执行验证
  if (!event.path?.startsWith("/api")) {
    return;
  }
  //排除路由
  if (event.path?.startsWith("/api/login")) {
    return;
  }
  //获取token
  const token = getCookie(event, "auth_token") || "";
  if (!token) {
    // 如果没有 token,返回未授权状态
    return { status: 400, message: "token验证失败" };
  }
  try {
    // 验证 token
    const decoded = jwt.verify(token, process.env.JWT_SECRET as string);
    // 如果需要,可以将解码后的信息附加到请求上下文
    event.context.verifyToken = decoded;
  } catch (err) {
    console.log(err, "error");
    // 验证失败,返回未授权状态
    return { status: 400, message: "token验证失败" };
  }
});

❗这里我们需要注意的是我们要排除路由,其实我们访问的http://localhost:3000/也是接口,不过nuxt给我们返回的是一个页面,如果我们不使用!event.path?.startsWith("/api")判断那么我们直接访问http://localhost:3000/也会进行拦截。同时我们也要拦截我们的登录接口,当然如果有其他不需要验证的接口可以继续添加,如果接口很多那么可以创建数组进行遍历

4-请求接口验证Token

在这里我们需要创建文件,路径如下:server/api/todo/add.ts和server/api/todo/todoModel.ts(这里文件目录和之前的教程Nuxt后端接口实战:从0到1连接MongoDB数据库|技术踩坑与完整代码示例相似,但是代码有所更改!)

server/api/todo/todoModel.ts代码如下:

import mongoose from "mongoose";

// 定义Todo模型的Schema
const todoSchema = new mongoose.Schema(
  {
    title: {
      type: String,
      required: true,
    },
    completed: {
      type: Boolean,
      default: false,
    },
    content: {
      type: String,
      default: "",
    },
    userId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "UserList",
    },
  },
  { timestamps: true } // 自动添加 createdAt 和 updatedAt 字段
);

todoSchema.index({ userId: 1, title: 1 });
// 使用mongoose.model()创建模型,避免重复定义模型的问题
const Todo = mongoose.model("Todo", todoSchema);

export default Todo;

server/api/todo/add.ts代码如下:

import { defineEventHandler, readBody } from "h3";
import Todo from "./todoModel";
export default defineEventHandler(async (event) => {
  try {
    const body = await readBody(event);
    if (!body.title) {
      return {
        status: 400,
        message: "填写标题",
      };
    }

    const verifyToken = await event.context.verifyToken;
    const todo = await Todo.create({
      title: body.title,
      content: body.content,
      userId: verifyToken.id,
    });

    return { status: 200, message: "添加成功", data: todo };
  } catch (err) {
    console.error("Error creating todo:", err);
    throw createError({
      statusCode: 500,
      statusMessage: "服务器错误",
    });
  }
});

这里我们需要注意的是const verifyToken = await event.context.verifyToken;我们之前在中间件中设置了上下文,我们在这里可以直接访问到。

5-前端请求接口

我们只用正常调用代码即可,复杂样式就不编写了,这里使用了Tailwind CSS,代码如下:

<template>
  <div class="flex justify-between w-full h-[100vh]">
    <main class="flex flex-1 justify-center items-center bg-white">
      <div
        class="w-[300px] h-[300px] flex justify-center items-center flex-col gap-4 p-3 shadow"
      >
        <label class="flex flex-col gap-2 w-full">
          <span>账号</span>
          <input type="text" class="pl-2" v-model="account" />
        </label>
        <label class="flex flex-col gap-2 w-full">
          <span>密码</span>
          <input type="password" class="pl-2" v-model="password" />
        </label>
        <button class="btn-primary" @click="submitFn">登录</button>
      </div>
    </main>
  </div>
</template>
<script setup lang="ts">
const account = ref("");
const password = ref("");
const submitFn = async () => {
  if (!account.value || !password.value) {
    alert("账号密码不能为空");
    return;
  }
  const { data } = await useAsyncData(() =>
    $fetch("/api/login", {
      method: "post",
      body: JSON.stringify({
        account: account.value,
        password: password.value,
      }),
    })
  );
  if (data.value && data.value.status === 200) {
    alert(data.value.message || "登录成功");
  }
};
</script>

这时候大家可能会注意到我们的submitFn这个函数我们在请求接口时候值直接传递了账号信息,没有在headers中设置token,是因为我们通过中间件直接获取了Cookie,所以不需要在headers中设置token。

结语

在这里需要注意几点:

🚨坑1:密钥不要太过简单,容易造成泄露风险

🚨坑2:Token过期时间设置过长,例如999天等

🚨坑3:前端存储不当,使用本地存储等,建议使用HttpOnly Cookie存储配合CSRF Token双重防护

欢迎大家来我的博客参观,我在进行百日知识学习,希望每天可以进步一点点!

如果您喜欢我写的文章不妨点赞关注加收藏,您的支持是我继续努力和分享的动力!

评论获取源码原与君共勉。

热门文章