动态

详情 返回 返回

二、nextjs API路由如何做好JWT登錄鑑權、身份鑑權,joi字段校驗,全局處理異常等(c-shopping電商開源) - 动态 详情

介紹

在這篇文章中,我們將學習如何在C-Shopping電商開源項目中,基於Next.js 14,處理所有API路由中添加身份驗證和錯誤處理中間件的思路與實現。

這篇文章中的代碼片段取自我最近開源項目C-Shopping,完整的項目和文檔可在https://github.com/huanghanzhilian/c-shopping地址查看。

Next.js中的API路由

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

例如文件 /app/api/user/route.js 會自動映射到路由 /api/user。API路由處理程序導出一個默認函數,該函數傳遞給HTTP請求處理程序。

有關Next.js API路由的更多信息,請參閲 https://nextjs.org/docs/app/building-your-application/routing/route-handlers

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

下面是一個API路由處理程序的基本示例,它將用户列表返回給HTTP GET請求。

只需要導出一個支持HTTP協議名稱,再返回一個Response,就完成了一個API

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

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

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

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

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

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

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

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

const setJson = ({ code, message, data } = {}) => {
  return {
    code: code || 0,
    message: message || 'ok',
    data: data || null,
  }
}

export { setJson }

至此,我們已經完成了API的設計,這將會給後期的開發帶來效率,但同時也帶來了代碼的難以理解度,只能説設計程序需要有取捨,合適就好。這是我自己基於Next.js Route 的一些設計,也歡迎大家一起通過探討。

Add a new 评论

Some HTML is okay.