二、nextjs API路由如何做好JWT登录鉴权、身份鉴权,joi字段校验,全局处理异常等(c-shopping电商开源)

介绍

在这篇文章中,我们将学习如何在C-Shopping电商开源项目中,基于Next.js 14,处理所有API路由中添加身份验证和错误处理中间件的思路与实现。

这篇文章中的代码片段取自我最近开源项目C-Shopping,完整的项目和文档可在github.com/huanghanzhi...地址查看。

Next.js中的API路由

在Next.js14中,/app/api 文件夹包含所有基于文件名路由的api接口

例如文件 /app/api/user/route.js 会自动映射到路由 /api/user。API路由处理程序导出一个默认函数,该函数传递给HTTP请求处理程序。

有关Next.js API路由的更多信息,请参阅 nextjs.org/docs/app/bu...

官方示例Next.js API 路由处理程序

下面是一个API路由处理程序的基本示例,它将用户列表返回给HTTP GET请求。

只需要导出一个支持HTTP协议名称,再返回一个Response,就完成了一个API

javascript 复制代码
export async function GET() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
  })
  const data = await res.json()
 
  return Response.json({ data })
}

Next.js 自定义编码设计 API处理器

我们会发现,如果按照官方的文档来写API,虽然简单,但是毫无设计感,当面对复杂项目时候很多引用会重复出现,我们需要设计一些中间间,来帮助我们更好的扩展API编码。

为了增加对中间件的支持,我创建了apiHandler包装器函数,该包装器接受一个API处理程序对象,并返回一个HTTP方法(例如GETPOSTPUTDELETE等),再到route文件导出该API,这样就既简单又高效的做好了基础的编码设计。

通过apiHandler包装器函数,再扩展了jwtMiddlewareidentityMiddlewarevalidateMiddlewareerrorHandler,来更好的设计优化代码:

  • jwtMiddleware(处理JWT校验);
  • identityMiddleware(处理身份校验);
  • validateMiddleware(处理 joi,字段校验);
  • errorHandler(全局处理异常)。

项目中的路径 /helpers/api/api-handler.js

javascript 复制代码
import { NextRequest, NextResponse } from 'next/server'

import { errorHandler, jwtMiddleware, validateMiddleware, identityMiddleware } from '.'

export { apiHandler }

function isPublicPath(req) {
  // public routes that don't require authentication
  const publicPaths = ['POST:/api/auth/login', 'POST:/api/auth/logout', 'POST:/api/auth/register']
  return publicPaths.includes(`${req.method}:${req.nextUrl.pathname}`)
}

function apiHandler(handler, { identity, schema, isJwt } = {}) {
  return async (req, ...args) => {
    try {
      if (!isPublicPath(req)) {
        // global middleware
        await jwtMiddleware(req, isJwt)
        await identityMiddleware(req, identity, isJwt)
        await validateMiddleware(req, schema)
      }
      // route handler
      const responseBody = await handler(req, ...args)
      return NextResponse.json(responseBody || {})
    } catch (err) {
      console.log('global error handler', err)
      // global error handler
      return errorHandler(err)
    }
  }
}

users [id] API路由处理程序

下面代码我们可以看到,使用了apiHandler包装器

  • 第一个参数是当前HTTP请求的核心逻辑,解析bodyqueryparams,查询数据,最后通过统一的setJson返回数据结构
  • 第二个参数是一个对象,里面包含了一些中间层扩展参数逻辑,isJwt是否需要JWT校验、schema需要校验的字段和类型、identity操作的用户是否符合权限等。

项目中的路径 /app/api/user/[id]/route.js

csharp 复制代码
import joi from 'joi'

import { usersRepo, apiHandler, setJson } from '@helpers'

const updateRole = apiHandler(
  async (req, { params }) => {
    const { id } = params
    const body = await req.json()
    await usersRepo.update(id, body)

    return setJson({
      message: '更新成功',
    })
  },
  {
    isJwt: true,
    schema: joi.object({
      role: joi.string().required().valid('user', 'admin'),
    }),
    identity: 'root',
  }
)

const deleteUser = apiHandler(
  async (req, { params }) => {
    const { id } = params
    await usersRepo.delete(id)
    return setJson({
      message: '用户信息已经删除',
    })
  },
  {
    isJwt: true,
    identity: 'root',
  }
)

export const PATCH = updateRole
export const DELETE = deleteUser
export const dynamic = 'force-dynamic'

Next.js jwtMiddleware 授权中间件

项目中JWT身份验证中间件是使用jsonwebtoken库来验证发送到受保护API路由的请求中的JWT令牌,如果令牌无效,则抛出错误,导致全局错误处理程序返回401 Unauthorized响应。JWT中间件被添加到API处理程序包装函数中的Next.js请求管道中。

项目中的路径:/api/jwt-middleware.js

javascript 复制代码
import { auth } from '../'

async function jwtMiddleware(req, isJwt = false) {
  const id = await auth.verifyToken(req, isJwt)
  req.headers.set('userId', id)
}
export { jwtMiddleware }

项目中的路径:/helpers/auth.js

javascript 复制代码
import jwt from 'jsonwebtoken'

const verifyToken = async (req, isJwt) => {
  try {
    const token = req.headers.get('authorization')
    const decoded = jwt.verify(token, process.env.NEXT_PUBLIC_ACCESS_TOKEN_SECRET)
    const id = decoded.id
    return new Promise(resolve => resolve(id))
  } catch (error) {
    if (isJwt) {
      throw error
    }
  }
}

const createAccessToken = payload => {
  return jwt.sign(payload, process.env.NEXT_PUBLIC_ACCESS_TOKEN_SECRET, {
    expiresIn: '1d',
  })
}

export const auth = {
  verifyToken,
  createAccessToken,
}

Next.js identityMiddleware 身份校验中间件

在项目设计中,暂时只设计了user普通用户、admin管理员用户,以及一个超级管理员权限root字段,在apiHandler()包装器函数调用时,可以来控制该接口的权限以及身份。

如果权限不匹配,将抛出全局错误,进入Next.js请求管道中,交给全局错误处理程序,从而做到接口异常处理。

项目中的路径:/helpers/api/identity-middleware.js

csharp 复制代码
import { usersRepo } from '../db-repo'

async function identityMiddleware(req, identity = 'user', isJwt = false) {
  if (identity === 'user' && isJwt === false) return

  const userId = req.headers.get('userId')
  const user = await usersRepo.getOne({ _id: userId })
  req.headers.set('userRole', user.role)
  req.headers.set('userRoot', user.root)

  if (identity === 'admin' && user.role !== 'admin') {
    throw '无权操作'
  }

  if (identity === 'root' && !user.root) {
    throw '无权操作,仅超级管理可操作'
  }
}

export { identityMiddleware }

Next.js validateMiddleware 请求参数校验中间件

apiHandler()包装器函数调用时,通过joi工具,schema参数,来指定需要接收和校验的参数,从而避免一些冗余的字段传递,减少异常的发生。

项目中的路径:/helpers/api/validate-middleware.js

javascript 复制代码
import joi from 'joi'

export { validateMiddleware }

async function validateMiddleware(req, schema) {
  if (!schema) return

  const options = {
    abortEarly: false, // include all errors
    allowUnknown: true, // ignore unknown props
    stripUnknown: true, // remove unknown props
  }

  const body = await req.json()
  const { error, value } = schema.validate(body, options)

  if (error) {
    throw `Validation error: ${error.details.map(x => x.message).join(', ')}`
  }

  // update req.json() to return sanitized req body
  req.json = () => value
}

Next.js全局错误处理程序

使用全局错误处理程序捕获所有错误,并消除了在整个Next.js API中重复错误处理代码的需要。

通常按照惯例,'string'类型的错误被视为自定义(特定于应用程序)错误,这简化了抛出自定义错误的代码,因为只需要抛出一个字符串(例如抛出'Username或password is incorrect'),如果自定义错误以'not found'结尾,则返回404响应代码,否则返回标准的400错误响应。

如果错误是一个名为"UnauthorizedError"的对象,则意味着JWT令牌验证失败,因此HTTP 401未经授权的响应代码将返回消息"无效令牌"。

所有其他(未处理的)异常都被记录到控制台,并返回一个500服务器错误响应代码。

项目中的路径:/helpers/api/error-handler.js

php 复制代码
import { NextResponse } from 'next/server'
import { setJson } from './set-json'

export { errorHandler }

function errorHandler(err) {
  if (typeof err === 'string') {
    // custom application error
    const is404 = err.toLowerCase().endsWith('not found')
    const status = is404 ? 404 : 400
    return NextResponse.json(
      setJson({
        message: err,
        code: status,
      }),
      { status }
    )
  }

  if (err.name === 'JsonWebTokenError') {
    // jwt error - delete cookie to auto logout
    return NextResponse.json(
      setJson({
        message: 'Unauthorized',
        code: '401',
      }),
      { status: 401 }
    )
  }

  if (err.name === 'UserExistsError') {
    return NextResponse.json(
      setJson({
        message: err.message,
        code: '422',
      }),
      { status: 422 }
    )
  }

  // default to 500 server error
  console.error(err)
  return NextResponse.json(
    setJson({
      message: err.message,
      code: '500',
    }),
    { status: 500 }
  )
}

Next.js 统一处理NextResponse,灵活统一使用setJson

为什么要这样设计?我们不想在每个route中,来回的去引用NextResponse,这会使得代码可读性很差,所以在apiHandler包装器函数中,调用了HTTP handler,拿到了路由管道中想要的数据,最后统一输出。

项目中的路径:/helpers/api/set-json.js

javascript 复制代码
const setJson = ({ code, message, data } = {}) => {
  return {
    code: code || 0,
    message: message || 'ok',
    data: data || null,
  }
}

export { setJson }

至此,我们已经完成了API的设计,这将会给后期的开发带来效率,但同时也带来了代码的难以理解度,只能说设计程序需要有取舍,合适就好。这是我自己基于Next.js Route 的一些设计,也欢迎大家一起通过探讨。

相关推荐
修己xj8 小时前
Anki:让记忆更高效、更智能的开源力量
开源
冬奇Lab13 小时前
一天一个开源项目(第17篇):ViMax - 多智能体视频生成框架,导演、编剧、制片人全包
开源·音视频开发
一个处女座的程序猿15 小时前
AI之Agent之VibeCoding:《Vibe Coding Kills Open Source》翻译与解读
人工智能·开源·vibecoding·氛围编程
一只大侠的侠16 小时前
React Native开源鸿蒙跨平台训练营 Day16自定义 useForm 高性能验证
flutter·开源·harmonyos
IvorySQL17 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
一只大侠的侠17 小时前
Flutter开源鸿蒙跨平台训练营 Day11从零开发商品详情页面
flutter·开源·harmonyos
一只大侠的侠17 小时前
React Native开源鸿蒙跨平台训练营 Day18自定义useForm表单管理实战实现
flutter·开源·harmonyos
一只大侠的侠18 小时前
React Native开源鸿蒙跨平台训练营 Day20自定义 useValidator 实现高性能表单验证
flutter·开源·harmonyos
晚霞的不甘18 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频
天下代码客19 小时前
使用electronc框架调用dll动态链接库流程和避坑
前端·javascript·vue.js·electron·node.js