WEB3全栈开发——面试专业技能点P6后端框架 / 微服务设计
一、Express
Express是国内大部分公司重点问的。我在本文最后,单独讲解了Express框架。
概念介绍
Express 是基于 Node.js 平台的极简、灵活且广泛使用的 Web 应用框架。它提供了一系列强大的功能,用于构建单页、多页及混合型的 Web 应用程序和 API 服务。
Express 的核心特点包括:
-
简洁易用的路由系统
-
中间件机制,方便请求处理和功能扩展
-
灵活的模板引擎支持
-
支持多种 HTTP 请求方法和路径匹配
-
兼容性强,易于与各种数据库和前端框架集成
Express 通常被用作后端 Web 服务的骨架,尤其在构建 RESTful API 和微服务架构时非常流行。
示例代码
下面是一个简单的 Express 服务器示例,实现一个基础的 GET 请求接口:
const express = require('express');
const app = express();
const port = 3000;// 定义路由,处理 GET 请求
app.get('/', (req, res) => {res.send('Hello, Express!');
});// 启动服务器监听端口
app.listen(port, () => {console.log(`Express server listening at http://localhost:${port}`);
});
讲解总结
-
路由管理:Express 通过
app.get
,app.post
等方法定义 URL 路径对应的请求处理函数,支持参数、查询字符串等。 -
中间件机制:Express 支持中间件函数,可以拦截请求,实现功能如身份验证、日志记录、请求体解析等。
-
简洁高效:相比原生 Node.js HTTP 模块,Express 大幅简化了代码量和复杂度,提升开发效率。
-
灵活扩展:拥有庞大的生态系统,可集成多种第三方中间件与插件,满足各种业务需求。
-
广泛应用:常用于构建 RESTful API、前后端分离架构的后端服务,及微服务组件。
Express 是 Node.js Web 开发的基础框架,掌握它对后端开发非常关键。
二、Koa
概念介绍
Koa 是由 Express 原班人马开发的下一代 Node.js Web 框架,设计目标是打造一个更小、更富表现力、更健壮的基础框架。它利用现代 JavaScript 的 async/await 特性,简化异步流程控制,摒弃了传统中间件的回调嵌套问题,提升代码的可读性和维护性。
Koa 本身非常轻量,不内置中间件,开发者可以根据需要自由组合,具有极高的灵活性。
示例代码
下面是一个使用 Koa 创建的简单服务器,响应 GET 请求:
const Koa = require('koa');
const app = new Koa();
const port = 3000;// 定义中间件,处理请求
app.use(async (ctx) => {if (ctx.path === '/') {ctx.body = 'Hello, Koa!';} else {ctx.status = 404;ctx.body = 'Not Found';}
});// 启动服务器监听端口
app.listen(port, () => {console.log(`Koa server running at http://localhost:${port}`);
});
讲解总结
-
现代异步处理:Koa 使用 async/await 处理异步代码,避免回调地狱,使代码更简洁易懂。
-
洋葱模型中间件:中间件执行遵循洋葱模型(洋葱圈层),支持在请求进入和响应返回时进行处理,便于实现日志、错误捕获、响应压缩等功能。
-
极简核心:Koa 只提供核心功能,不包含路由、中间件等,开发者可根据业务需求灵活引入,打造定制化架构。
-
更好错误处理:通过 async 函数的错误捕获机制,Koa 能优雅地处理异步错误,提升程序稳定性。
-
适合微服务:Koa 的灵活性和简洁性非常适合用来构建轻量级的微服务或 API 服务。
Koa 是 Node.js 生态中注重现代语法与灵活设计的 Web 框架,适合对代码质量和扩展性有较高要求的项目。
三、NestJS 的模块化架构
概念介绍
NestJS 是一个基于 TypeScript 构建的进阶 Node.js 框架,借鉴了 Angular 的设计理念,采用模块化架构来组织应用。模块(Module)是 NestJS 应用的基本组成单元,每个模块封装了一组相关的功能,包括控制器(Controllers)、服务(Providers)、导入的其他模块等。
模块化架构有助于分离关注点,提升代码的复用性和可维护性,使大型应用易于管理和扩展。
示例代码
下面是一个简单的模块定义示例,展示如何创建和使用模块:
import { Module, Injectable, Controller, Get } from '@nestjs/common';// 服务层,提供业务逻辑
@Injectable()
export class HelloService {getHello(): string {return 'Hello, NestJS Module!';}
}// 控制器层,处理请求
@Controller()
export class HelloController {constructor(private readonly helloService: HelloService) {}@Get()getHello(): string {return this.helloService.getHello();}
}// 定义模块,组织控制器和服务
@Module({imports: [], // 导入其他模块controllers: [HelloController],providers: [HelloService],exports: [HelloService], // 可导出给其他模块使用
})
export class HelloModule {}
讲解总结
-
模块(Module) 是 NestJS 应用的组织单位,使用
@Module
装饰器定义,包含控制器、服务和导入的模块。 -
控制器(Controller) 负责处理客户端请求,定义路由和请求方法。
-
服务(Provider) 封装业务逻辑,支持依赖注入(DI),解耦业务与控制层。
-
模块之间通过导入(imports)和导出(exports)实现功能复用和共享,方便拆分大型应用为多个独立子模块。
-
模块化架构提高应用可维护性和扩展性,便于团队协作和功能拆分。
-
NestJS 的模块设计结合了依赖注入和面向对象编程思想,令开发体验更现代化且高效。
掌握 NestJS 的模块化架构是构建清晰、结构良好的企业级应用的基础。
四、NestJS的依赖注入
概念介绍
依赖注入(Dependency Injection,简称 DI)是一种设计模式,通过将对象的依赖(例如服务)由框架自动提供,而不是由对象自行创建,从而实现代码解耦和模块间松耦合。
NestJS 内置强大的依赖注入容器,自动管理服务实例的创建和生命周期,使组件之间的依赖关系清晰且易于维护。通过构造函数注入(constructor injection)是 NestJS DI 的核心方式。
示例代码
下面示例展示如何在 NestJS 中通过依赖注入使用服务:
import { Injectable, Controller, Get } from '@nestjs/common';// 定义服务,提供业务逻辑
@Injectable()
export class UserService {getUser() {return { id: 1, name: 'Alice' };}
}// 定义控制器,依赖注入 UserService
@Controller('users')
export class UserController {constructor(private readonly userService: UserService) {}@Get()getUser() {return this.userService.getUser();}
}
讲解总结
-
@Injectable()
装饰器标记服务类,使其可以被 NestJS 容器管理和注入。 -
构造函数注入:依赖通过控制器或其他服务的构造函数参数声明,NestJS 自动实例化并传入对应依赖。
-
依赖注入容器会根据作用域(默认单例)管理服务实例,避免重复创建,提高性能。
-
依赖注入解耦了类与其依赖,实现高内聚低耦合,有利于单元测试和代码维护。
-
NestJS 还支持自定义作用域(如请求作用域)和手动注入(通过
@Inject()
装饰器),增强灵活性。
依赖注入是 NestJS 核心设计之一,掌握它可以大幅提升项目结构的清晰度和扩展性。
五、NestJS的守卫
概念介绍
守卫(Guard)是 NestJS 中用于控制请求权限的机制,类似于中间件,但专注于授权和权限检查。守卫可以决定请求是否可以继续执行路由处理逻辑,通常用于身份验证、角色权限校验等场景。
守卫实现 CanActivate
接口,返回 true
允许请求继续,返回 false
或抛出异常则拒绝请求。
示例代码
下面示例展示一个简单的守卫,用于检查请求头中是否包含特定令牌:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';@Injectable()
export class AuthGuard implements CanActivate {canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {const request = context.switchToHttp().getRequest();const token = request.headers['authorization'];// 简单校验:请求头必须有指定的 tokenreturn token === 'my-secret-token';}
}
在控制器中使用守卫:
import { Controller, Get, UseGuards } from '@nestjs/common';@Controller('profile')
@UseGuards(AuthGuard) // 作用于整个控制器
export class ProfileController {@Get()getProfile() {return { name: 'Alice', role: 'admin' };}
}
讲解总结
-
守卫通过实现
CanActivate
接口控制请求是否被处理,适合做权限、认证逻辑。 -
通过
ExecutionContext
获取请求信息(如请求头、用户信息等)。 -
守卫返回
true
允许请求继续,返回false
或抛异常拒绝请求。 -
守卫可以作用于控制器类或单个路由方法,支持灵活配置。
-
NestJS 结合守卫和中间件、拦截器等机制,实现强大的请求生命周期管理。
掌握守卫可帮助构建安全、可控的后端服务,确保敏感接口仅授权用户访问。
六、NestJS 的拦截器
概念介绍
拦截器(Interceptor)是 NestJS 中一种强大的功能,用于拦截和处理函数调用前后逻辑。拦截器可以用于:
-
修改方法输入参数或返回结果
-
实现日志记录、缓存、异常处理、性能监控
-
对请求进行额外处理或响应包装
拦截器实现 NestInterceptor
接口,核心方法 intercept()
接收 ExecutionContext
和 CallHandler
,通过 RxJS 操作符处理请求流。
示例代码
下面示例是一个简单的日志拦截器,打印请求开始和结束时间:
import {Injectable,NestInterceptor,ExecutionContext,CallHandler,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';@Injectable()
export class LoggingInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {console.log('Before handling request...');const now = Date.now();return next.handle().pipe(tap(() => console.log(`After handling request... ${Date.now() - now}ms`)),);}
}
使用拦截器:
import { Controller, Get, UseInterceptors } from '@nestjs/common';@Controller('items')
@UseInterceptors(LoggingInterceptor)
export class ItemsController {@Get()findAll() {return ['item1', 'item2'];}
}
讲解总结
-
拦截器在请求处理前后执行,能够操作请求和响应数据流。
-
通过 RxJS 操作符(如
tap
、map
)可以异步处理响应。 -
拦截器广泛应用于日志、缓存、异常转换、数据格式化等场景。
-
可以作用于全局、控制器、单个路由,支持灵活配置。
-
结合守卫和管道,构成 NestJS 完整请求处理链。
掌握拦截器能够极大增强应用的功能扩展性和代码复用性。
七、Express 的模块化架构
概念介绍
Express 默认是一个轻量级的 Node.js Web 框架,支持快速搭建服务器和路由。模块化架构指的是将应用拆分为多个功能模块,每个模块独立管理路由、控制器和中间件,便于代码维护、复用和团队协作。
核心思想:
-
路由拆分:每个模块有自己独立路由文件,负责特定业务路由。
-
控制器分离:处理业务逻辑的函数单独放置,保持路由简洁。
-
中间件复用:公共功能用中间件抽象,跨模块复用。
-
按功能组织代码:目录结构清晰,易于扩展。
模块化架构让大型项目更易维护,同时也方便测试和协作。
示例代码
假设一个简单的用户模块和商品模块,拆分路由和控制器。
目录结构示例
/app/controllersuserController.jsproductController.js/routesuserRoutes.jsproductRoutes.jsapp.js // app.js 中挂载路由前缀(主入口)
userController.js
// 处理用户相关业务逻辑
// controllers/userController.js
exports.getUser = (req, res) => {const userId = req.params.id;// 模拟获取用户信息res.json({ id: userId, name: 'Alice' });
};
productController.js
// 处理商品相关业务逻辑
exports.getProduct = (req, res) => {const productId = req.params.id;// 模拟获取商品信息res.json({ id: productId, name: 'Phone', price: 599 });
};
userRoutes.js(在app.js中绑定了路径前缀 /users
)
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
// 实际请求 URL: GET /users/:id
router.get('/:id', userController.getUser);module.exports = router;
productRoutes.js(在app.js中绑定了路径前缀 /products
)
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');// 实际请求 URL: GET /products/:id
router.get('/:id', productController.getProduct);module.exports = router;
app.js中挂载路由(主入口)
const express = require('express');
const app = express();const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');// 路由前缀绑定
app.use('/users', userRoutes); // 所有 user 路由以 /users 开头
app.use('/products', productRoutes); // 所有 product 路由以 /products 开头app.listen(3000, () => {console.log('Server running on port 3000');
});
实际完整 URL 路径
结合以上配置:
功能 | 请求方法 | 完整 URL 示例 | 控制器函数 |
---|---|---|---|
获取用户 | GET | http://localhost:3000/users/123 | getUser() |
获取商品 | GET | http://localhost:3000/products/456 | getProduct() |
如果你还想加 POST、PUT、DELETE 这类方法,也可以在路由里扩展,例如:
router.post('/', userController.createUser); // POST /users
讲解总结
-
职责分明:路由只负责请求分发,业务逻辑放在控制器,代码层次清晰。
-
易于维护:模块化结构使代码易读,方便多人协作和后续功能扩展。
-
复用性强:公共中间件可跨模块使用,提高代码复用率。
-
便于测试:模块化让单元测试和集成测试更加简便。
Express 模块化架构适合中大型项目,是构建可维护、扩展性好的 Node.js 应用的推荐方式。
八、Express 的依赖注入
✅ 概念介绍:Express 的依赖注入
Express 本身并不内建依赖注入机制,它是一个极简主义框架。与 NestJS 不同,NestJS 是基于 Angular 风格的完整依赖注入系统构建的。但在 Express 中,你可以使用一些第三方库(如 awilix、inversify 等)手动实现依赖注入,来提升项目的模块化与可测试性。
依赖注入(DI)的目标是:将对象之间的依赖关系“注入”而非硬编码,让代码更解耦、更好测试、更易维护。
✅ 示例代码(使用 awilix
实现 Express 的依赖注入)
1. 安装依赖
npm install awilix awilix-express
2. 项目结构
app/
├── app.js
├── routes/
│ └── userRoutes.js
├── controllers/
│ └── userController.js
├── services/
│ └── userService.js
└── container.js
3. 创建服务层(业务逻辑)
// services/userService.js
class UserService {getUser(id) {return { id, name: 'Alice (DI)' };}
}module.exports = UserService;
4. 创建控制器(接收依赖)
// controllers/userController.js
class UserController {constructor({ userService }) {this.userService = userService;}getUser = (req, res) => {const user = this.userService.getUser(req.params.id);res.json(user);};
}module.exports = UserController;
5. 设置 Awilix 容器
// container.js
const { createContainer, asClass } = require('awilix');
const UserService = require('./services/userService');
const UserController = require('./controllers/userController');const container = createContainer();container.register({userService: asClass(UserService).scoped(),userController: asClass(UserController).scoped(),
});module.exports = container;
6. 路由绑定(通过 awilix-express)
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { makeInvoker } = require('awilix-express');// 控制器加载器
const container = require('../container');
const userController = makeInvoker(container.resolve('userController'));router.get('/:id', userController('getUser'));module.exports = router;
7. 应用入口(挂载路由)
// app.js
const express = require('express');
const { scopePerRequest } = require('awilix-express');
const container = require('./container');
const userRoutes = require('./routes/userRoutes');const app = express();
app.use(scopePerRequest(container)); // 关键 DI 挂载点
app.use('/users', userRoutes);app.listen(3000, () => {console.log('Server running on http://localhost:3000');
});
✅ 总结
项目结构 | 描述 |
---|---|
userService.js | 提供独立服务逻辑,可复用 |
userController.js | 通过构造函数自动注入依赖 |
container.js | 中央依赖注入容器 |
awilix-express | 把 DI 自动接入 Express 生命周期中 |
是否需要我补充 inversify
版本、或者如何结合 JWT
、数据库服务
等依赖的注入结构?
九、Express的守卫
✅ 概念介绍:Express 的守卫(Guard)
在 NestJS 中,“守卫”(Guard)是用来控制某个请求是否有权限访问的类。但在 Express 中没有“守卫”这一专有概念,不过你可以用 中间件(Middleware) 实现类似“守卫”的功能。
Express 中的“守卫”常用于:
-
登录校验(是否带有 Token)
-
权限校验(是否管理员)
-
请求频率限制、接口黑白名单控制等
✅ 示例代码:实现 Express 中的“守卫”功能
🎯 目标:实现一个 JWT 鉴权“守卫”
我们将创建一个中间件,验证请求是否携带合法的 JWT Token。
1. 安装依赖
npm install jsonwebtoken
2. 编写守卫中间件(authGuard.js
)
// middlewares/authGuard.js
const jwt = require('jsonwebtoken');
const SECRET = 'your_secret_key'; // 应放在 .env 环境变量中const authGuard = (req, res, next) => {const authHeader = req.headers['authorization'];if (!authHeader || !authHeader.startsWith('Bearer ')) {return res.status(401).json({ message: '未提供有效 Token' });}const token = authHeader.split(' ')[1];try {const payload = jwt.verify(token, SECRET);req.user = payload; // 可用于后续控制器中next(); // 放行} catch (err) {return res.status(403).json({ message: 'Token 无效或已过期' });}
};module.exports = authGuard;
3. 使用守卫中间件保护路由
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const authGuard = require('../middlewares/authGuard');router.get('/profile', authGuard, (req, res) => {// 只有验证通过的用户才能访问res.json({ message: `欢迎你,${req.user.username}` });
});module.exports = router;
4. 登录接口生成 Token 示例
// routes/auth.js
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();router.post('/login', (req, res) => {const { username, password } = req.body;// 真实项目应校验数据库if (username === 'admin' && password === '123456') {const token = jwt.sign({ username, role: 'admin' }, 'your_secret_key', { expiresIn: '2h' });return res.json({ token });}res.status(401).json({ message: '账号或密码错误' });
});module.exports = router;
✅ 请求示例
1. 登录获取 Token
POST /login
Content-Type: application/json{"username": "admin","password": "123456"
}
响应:
{"token": "eyJhbGciOi..."
}
2. 访问受保护资源
GET /users/profile
Authorization: Bearer eyJhbGciOi...
✅ 总结
名称 | 实现方式 |
---|---|
守卫(Nest) | 使用 @Injectable() 类 |
守卫(Express) | 使用中间件函数(req , res , next ) |
应用场景 | JWT、权限控制、接口限流、黑白名单等 |
如果你想实现角色权限守卫、API 接口签名验证等高级“守卫”,我也可以继续帮你写完整示例。是否需要?
十、Express 拦截器
✅ 概念介绍:Express 的拦截器(Interceptor)
在 NestJS 中,“拦截器”是一个强大的功能,用于扩展请求/响应行为(如统一响应格式、日志记录、异常包装等)。
Express 虽没有原生“拦截器”这个名词,但我们可以通过 中间件(Middleware) 实现“拦截器”功能。
✅ 一句话理解:在 Express 中,“拦截器”是一个特定用途的中间件,用来在请求进入控制器之前/之后进行逻辑处理。
✅ 常见用途:
-
请求/响应日志记录
-
请求耗时分析
-
接口统一响应格式处理
-
异常捕获与封装
-
跨域处理
✅ 示例代码
🎯 示例:编写一个记录请求时间和统一响应格式的拦截器中间件
1. 日志与响应包装拦截器 interceptor.js
// middlewares/interceptor.js
module.exports = (req, res, next) => {const startTime = Date.now();// 重写 res.json 方法,实现统一结构const originalJson = res.json.bind(res);res.json = (data) => {const duration = Date.now() - startTime;return originalJson({code: 0,message: 'success',data,duration: `${duration}ms`});};next();
};
2. 应用拦截器中间件到 Express 应用
// app.js
const express = require('express');
const app = express();
const interceptor = require('./middlewares/interceptor');app.use(express.json());
app.use(interceptor); // 全局拦截器app.get('/api/hello', (req, res) => {res.json({ text: 'Hello World!' });
});app.listen(3000, () => {console.log('Server running on http://localhost:3000');
});
🧪 请求示例
GET /api/hello
💡 响应结果(统一格式):
{"code": 0,"message": "success","data": {"text": "Hello World!"},"duration": "2ms"
}
✅ 扩展用法:仅拦截特定路由
app.use('/api/secure', interceptor);
✅ 总结
功能 | Express 实现方式 |
---|---|
拦截器(请求 & 响应) | 中间件函数包裹 res.json 或 res.send |
执行顺序 | 注册顺序决定调用链,越早注册越早执行 |
特点 | 可用作全局或局部中间件 |
如需实现 异常处理拦截器、权限校验拦截器、链上接口统一响应结构 等,我也可以提供对应示例。需要的话告诉我即可。
十一、Express 的 JWT 设计链上链下鉴权系统
概念介绍
在 Web3 应用中,链上身份验证通常依赖区块链钱包签名消息(如 MetaMask 签名),而链下服务(如后端 API)使用 JWT(JSON Web Token)维护会话状态,实现权限控制。链上链下鉴权系统结合了这两者:
-
用户通过钱包签名证明身份(链上认证)
-
服务器验证签名后签发 JWT,用于后续链下请求鉴权
-
JWT 包含用户地址等信息,携带在请求头,服务器验证后允许访问受保护资源
这种设计避免每次请求都要求钱包签名,提高用户体验,同时保持安全性。
示例代码
以下示例用 Express 和 jsonwebtoken
实现简易链上链下鉴权流程:
const express = require('express');
const jwt = require('jsonwebtoken');
const { ethers } = require('ethers');const app = express();
app.use(express.json());const JWT_SECRET = 'your_jwt_secret';// 生成随机消息供客户端签名
app.get('/auth/message/:address', (req, res) => {const { address } = req.params;const message = `Login to MyDApp at ${Date.now()}`;// 这里应缓存 message 与 address 对应,用于验证res.json({ message });
});// 验证签名并签发 JWT
app.post('/auth/verify', (req, res) => {const { address, signature, message } = req.body;try {// 使用 ethers 验证签名者地址const signerAddress = ethers.utils.verifyMessage(message, signature);if (signerAddress.toLowerCase() !== address.toLowerCase()) {return res.status(401).json({ error: 'Invalid signature' });}// 签名合法,签发 JWTconst token = jwt.sign({ address }, JWT_SECRET, { expiresIn: '1h' });res.json({ token });} catch (error) {res.status(400).json({ error: 'Verification failed' });}
});// 受保护接口,验证 JWT
function authenticateToken(req, res, next) {const authHeader = req.headers['authorization'];const token = authHeader && authHeader.split(' ')[1];if (!token) return res.sendStatus(401);jwt.verify(token, JWT_SECRET, (err, user) => {if (err) return res.sendStatus(403);req.user = user; // 保存解码后的用户信息next();});
}app.get('/protected', authenticateToken, (req, res) => {res.json({ message: `Hello ${req.user.address}, this is protected data.` });
});app.listen(3000, () => {console.log('Server started on port 3000');
});
讲解总结
-
链上认证:用户通过钱包签名服务器发送的随机消息,证明对该地址的控制权。
-
链下鉴权:服务器验证签名后,使用 JWT 生成包含用户地址的令牌,客户端持有此令牌访问受保护接口。
-
JWT 验证:服务器中间件检查请求中的 JWT,保证请求合法且未过期。
-
优势:减少频繁签名操作,提升用户体验;同时保证安全与身份唯一性。
这种模式是典型的 Web3 应用鉴权方案,兼顾区块链身份验证与传统后端权限控制。
十二、Express 的钱包签名(MetaMask)设计链上链下鉴权系统
概念介绍
在 Web3 应用中,用户使用钱包(如 MetaMask)进行链上身份认证。通过钱包签名服务器随机生成的消息(challenge),证明其对某个以太坊地址的控制权。服务器验证签名后,生成链下的 JWT 令牌,用户凭此令牌访问后端受保护资源。
核心流程:
-
服务器生成随机消息(challenge)并发给客户端。
-
客户端用 MetaMask 连接钱包,签名该消息。
-
客户端将签名与地址发回服务器。
-
服务器验证签名,确认用户身份后,颁发 JWT。
-
后续请求携带 JWT 进行链下身份验证。
该方案结合链上签名的安全性和链下 JWT 的高效性,实现用户友好且安全的认证授权。
示例代码
const express = require('express');
const jwt = require('jsonwebtoken');
const { ethers } = require('ethers');const app = express();
app.use(express.json());const JWT_SECRET = 'your_jwt_secret';// 简单内存存储,实际项目应用数据库或缓存
const challenges = {};// 1. 客户端请求获取挑战消息
app.get('/auth/challenge/:address', (req, res) => {const { address } = req.params;const challenge = `登录验证消息:${Date.now()}`;challenges[address.toLowerCase()] = challenge;res.json({ challenge });
});// 2. 客户端提交签名和地址进行验证
app.post('/auth/verify', (req, res) => {const { address, signature } = req.body;const challenge = challenges[address.toLowerCase()];if (!challenge) {return res.status(400).json({ error: 'Challenge not found' });}try {// 验证签名是否匹配地址const signer = ethers.utils.verifyMessage(challenge, signature);if (signer.toLowerCase() !== address.toLowerCase()) {return res.status(401).json({ error: '签名验证失败' });}// 验证成功,签发 JWT,1 小时过期const token = jwt.sign({ address }, JWT_SECRET, { expiresIn: '1h' });// 可删除已使用的挑战,防止重放攻击delete challenges[address.toLowerCase()];res.json({ token });} catch (error) {res.status(400).json({ error: '签名验证异常' });}
});// 3. JWT 验证中间件
function authenticateToken(req, res, next) {const authHeader = req.headers['authorization'];if (!authHeader) return res.sendStatus(401);const token = authHeader.split(' ')[1];if (!token) return res.sendStatus(401);jwt.verify(token, JWT_SECRET, (err, user) => {if (err) return res.sendStatus(403);req.user = user;next();});
}// 4. 受保护资源示例
app.get('/protected', authenticateToken, (req, res) => {res.json({ message: `欢迎 ${req.user.address},访问受保护资源成功。` });
});app.listen(3000, () => {console.log('服务器运行于端口 3000');
});
讲解总结
-
挑战消息(challenge):防止重放攻击,确保每次认证的唯一性。
-
钱包签名:客户端用 MetaMask 调用
eth_sign
或personal_sign
签名 challenge,证明地址所有权。 -
签名验证:服务器用
ethers.utils.verifyMessage
验证签名对应的地址是否正确。 -
JWT 令牌:签名验证通过后,服务器生成 JWT,客户端持有该令牌访问后端资源,无需每次都签名。
-
安全防护:使用 challenge 阶段限制重放,JWT 过期和服务器验证保护接口安全。
这种设计模式是当前主流 Web3 应用链上链下鉴权方案,兼具安全性和使用便利。
十三、Express 构建 DApp 的后端微服务架构
概念介绍
DApp(去中心化应用)通常需要后端服务来处理链上数据索引、用户身份管理、业务逻辑处理等。使用 Express 构建后端微服务架构,意味着将系统拆分成多个小型服务模块,每个模块专注单一职责,通过 API 接口相互通信,便于维护、扩展和独立部署。
关键点:
-
模块化设计:每个微服务负责不同功能(如用户认证、交易处理、事件监听等)。
-
API 网关:统一入口,路由请求到不同微服务。
-
异步消息队列(如 RabbitMQ/Kafka)用于微服务间解耦和异步通信。
-
链上链下数据分离:微服务可专注链上事件处理或链下数据存储。
-
使用 JWT 或钱包签名做鉴权。
-
Docker 容器化部署,支持弹性扩缩。
示例代码
示例中展示一个简单的微服务结构示意,用 Express 实现用户服务和事件服务,并通过 HTTP 请求互相调用。
用户服务 user-service.js
const express = require('express');
const app = express();
app.use(express.json());app.post('/login', (req, res) => {// 处理用户登录,返回 tokenres.json({ token: 'user-jwt-token' });
});app.get('/profile/:address', (req, res) => {const { address } = req.params;// 查询用户链下数据res.json({ address, name: 'Alice', assets: [] });
});app.listen(3001, () => {console.log('User service running on port 3001');
});
事件服务 event-service.js
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());app.post('/process-event', async (req, res) => {const eventData = req.body;// 处理链上事件逻辑,比如入库、触发业务等// 调用用户服务示例:查询用户信息try {const response = await axios.get(`http://localhost:3001/profile/${eventData.userAddress}`);console.log('用户信息', response.data);} catch (err) {console.error('调用用户服务失败', err);}res.json({ status: 'event processed' });
});app.listen(3002, () => {console.log('Event service running on port 3002');
});
讲解总结
-
职责分离:用户身份管理与链上事件处理分别独立微服务,互不影响,方便独立维护和升级。
-
服务间通信:使用 HTTP REST 调用(示例中用 axios),也可用消息队列解耦。
-
扩展性好:服务可以水平扩展、独立部署,提高系统可用性和稳定性。
-
安全性:每个微服务独立实现鉴权机制,保护数据安全。
-
链上数据处理:事件服务负责监听链上事件,异步处理后写入链下数据库,优化响应速度。
-
容器化与自动化部署:结合 Docker 和 Kubernetes 做微服务编排和管理。
Express 结合微服务架构,是构建高效、灵活的 Web3 后端服务的常见方案。