二、nextjs API 路由如何做好 JWT 登录鉴权、身份鉴权, joi 字段校验,全局处理异常等 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
jixiaopeng
V2EX    程序员

二、nextjs API 路由如何做好 JWT 登录鉴权、身份鉴权, joi 字段校验,全局处理异常等

  •  
  •   jixiaopeng
    huanghanzhilian 2024-02-07 07:56:25 +08:00 2620 次点击
    这是一个创建于 611 天前的主题,其中的信息可能已经有所发展或是发生改变。

    介绍

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

    这篇文章中的代码片段取自我最近开源项目 C-Shopping

    完整的项目和文档可在https://github.com/huanghanzhilian/c-shopping 地址查看。

    项目在线演示地址:

    docker 部署地址:http://shop.huanghanlian.com/

    vercel 部署地址:https://c-shopping-three.vercel.app/

    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 的一些设计,也欢迎大家一起共同探讨。

    18 条回复    2024-02-13 05:12:28 +08:00
    Dragonphy
        1
    Dragonphy  
       2024-02-07 08:40:25 +08:00
    Next 最近人人喊打啊,都在推 Remix
    jixiaopeng
        2
    jixiaopeng  
    OP
       2024-02-07 08:42:22 +08:00
    @Dragonphy 我觉得能解决问题就是好的框架,就和谈对象一样,要看双方适不适合
    lstz
        3
    lstz  
       2024-02-07 09:34:21 +08:00 via Android
    怎么不上 ts
    jixiaopeng
        4
    jixiaopeng  
    OP
       2024-02-07 09:35:32 +08:00
    @lstz 后续会持续迭代,肯定要上 TS
    wu00
        5
    wu00  
       2024-02-07 09:56:35 +08:00
    asp.net core 一模一样
    jixiaopeng
        6
    jixiaopeng  
    OP
       2024-02-07 09:58:33 +08:00
    @wu00 不太了解 asp.net core ,要去学习学习了
    dream4ever
        7
    dream4ever  
       2024-02-07 10:03:57 +08:00
    写系列文章的话,可以在每一篇文章中把之前的各篇文章链接也附上,阅读体验会更好。
    jixiaopeng
        8
    jixiaopeng  
    OP
       2024-02-07 10:05:14 +08:00
    @dream4ever 谢谢老师的建议,我一定会改正,再次感激提醒
    horizon
        9
    horizon  
       2024-02-07 10:45:58 +08:00
    很好,我用 blitzjs
    jixiaopeng
        10
    jixiaopeng  
    OP
       2024-02-07 11:54:45 +08:00
    @horizon 感激很好的样子
    xiaojun996
        11
    xiaojun996  
       2024-02-07 14:21:10 +08:00
    不错不错,虽然我现在是用 nextjs + nestjs 开发了 2 个项目,没有用 nextjs 写 api ,不过你这个文章让我收获不少,谢谢 upup
    jixiaopeng
        12
    jixiaopeng  
    OP
       2024-02-07 15:01:15 +08:00 via iPhone
    @xiaojun996 哈哈,一起共同进步
    lilei2023
        13
    lilei2023  
       2024-02-07 17:06:48 +08:00
    页面路由权限怎么搞? 没找一个最佳的办法,尤其是在服务端发送请求判断的时候,
    jixiaopeng
        14
    jixiaopeng  
    OP
       2024-02-07 17:43:39 +08:00 via iPhone
    @lilei2023 您可以看看我开源项目源码,我是用 layout 组件来处理,后续我会更新这类文章。
    rizon
        15
    rizon  
       2024-02-09 07:43:24 +08:00 via iPhone
    文章挺好的。

    顺便随手感慨一句:js 这种语言果然可读性很差啊,ts 也就好个些许。python 这种缩进型的读起来也是痛苦。
    单说阅读,在各种语法糖的加持下,需要更多的理解消耗。有时候不能做到一眼扫一下就看完的效果。还需要脑内加工一下。
    就随便感慨一句。
    jixiaopeng
        16
    jixiaopeng  
    OP
       2024-02-09 09:55:15 +08:00 via iPhone
    @rizon 确实是,我在想我要尽快改成 ts
    cheunghy
        17
    cheunghy  
       2024-02-12 05:06:06 +08:00
    next 新出的 app 路由,坑太多了
    jixiaopeng
        18
    jixiaopeng  
    OP
       2024-02-13 05:12:28 +08:00 via iPhone
    @cheunghy 确实是,需要去踩踩,不过理解了感觉还行
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2930 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 12:50 PVG 20:50 LAX 05:50 JFK 08:50
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86