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'];
    // 简单校验:请求头必须有指定的 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 后端服务的常见方案。

相关推荐
Lee川4 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川7 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i9 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有10 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有10 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫11 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫11 小时前
Handler基本概念
面试
Wect11 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼12 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼12 小时前
Next.js 企业级落地
前端·javascript·面试