介紹
在這篇文章中,我們將學習如何在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方法(例如GET, POST, PUT, DELETE等),再到route文件導出該API,這樣就既簡單又高效的做好了基礎的編碼設計。
通過apiHandler包裝器函數,再擴展了jwtMiddleware、identityMiddleware、validateMiddleware、errorHandler,來更好的設計優化代碼:
- 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請求的核心邏輯,解析
body、query、params,查詢數據,最後通過統一的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 的一些設計,也歡迎大家一起通過探討。