WEB3全栈开发——面试专业技能点P6后端框架 / 微服务设计

发布于:2025-06-10 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、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'];
    // 简单校验:请求头必须有指定的 token
    return 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() 接收 ExecutionContextCallHandler,通过 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 操作符(如 tapmap)可以异步处理响应。

  • 拦截器广泛应用于日志、缓存、异常转换、数据格式化等场景。

  • 可以作用于全局、控制器、单个路由,支持灵活配置。

  • 结合守卫和管道,构成 NestJS 完整请求处理链。

掌握拦截器能够极大增强应用的功能扩展性和代码复用性。

七、Express 的模块化架构

概念介绍

Express 默认是一个轻量级的 Node.js Web 框架,支持快速搭建服务器和路由。模块化架构指的是将应用拆分为多个功能模块,每个模块独立管理路由、控制器和中间件,便于代码维护、复用和团队协作。

核心思想:

  • 路由拆分:每个模块有自己独立路由文件,负责特定业务路由。

  • 控制器分离:处理业务逻辑的函数单独放置,保持路由简洁。

  • 中间件复用:公共功能用中间件抽象,跨模块复用。

  • 按功能组织代码:目录结构清晰,易于扩展。

模块化架构让大型项目更易维护,同时也方便测试和协作。


示例代码

假设一个简单的用户模块和商品模块,拆分路由和控制器。

目录结构示例
/app
  /controllers
    userController.js
    productController.js
  /routes
    userRoutes.js
    productRoutes.js
  app.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 中,你可以使用一些第三方库(如 awilixinversify 等)手动实现依赖注入,来提升项目的模块化与可测试性。

依赖注入(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.jsonres.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' });
    }

    // 签名合法,签发 JWT
    const 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 令牌,用户凭此令牌访问后端受保护资源。

核心流程

  1. 服务器生成随机消息(challenge)并发给客户端。

  2. 客户端用 MetaMask 连接钱包,签名该消息。

  3. 客户端将签名与地址发回服务器。

  4. 服务器验证签名,确认用户身份后,颁发 JWT。

  5. 后续请求携带 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_signpersonal_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) => {
  // 处理用户登录,返回 token
  res.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 后端服务的常见方案。