You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Clean Architecture下Express+Node.js余额创建的用户认证实现疑问

Clean Architecture 下的 Token 验证与 Balance 创建最佳实践

你并没有误解 Clean Architecture,核心问题是要理清各层的职责边界,通过抽象端口+依赖倒置来解决分层依赖的冲突。下面是具体的实践方案:

首先明确分层边界(Clean Architecture 核心)

  1. 实体层:Balance 核心业务实体,只包含业务规则,不依赖任何外部层
  2. 用例层:CreateBalanceUseCase,负责核心业务逻辑,仅依赖实体和抽象端口(输入/输出端口),不碰任何具体实现
  3. 接口适配器层:控制器、中间件、服务实现、数据访问适配器,负责把外部系统(HTTP、数据库)的格式转换为用例层能识别的抽象接口
  4. 框架驱动层:Express、数据库驱动等,纯工具类,不包含业务逻辑

解决方案:抽象授权端口+中间件处理验证

1. 定义用例层的抽象授权端口

用例层只声明需要的能力,不关心具体实现:

// use-cases/ports/authorization.port.ts
export interface AuthorizationPort {
  // 验证Token并返回合法的userId
  validateToken(token: string): Promise<string>;
}

2. 在接口适配器层实现授权端口

这里可以安全访问数据层(比如查用户表验证Token有效性),因为接口适配器的职责就是适配外部依赖到抽象接口:

// interface-adapters/services/jwt-authorization.service.ts
import { AuthorizationPort } from '../../use-cases/ports/authorization.port';
import { UserRepository } from '../repositories/user.repository';
import jwt from 'jsonwebtoken';

export class JwtAuthorizationService implements AuthorizationPort {
  constructor(private readonly userRepo: UserRepository) {}

  async validateToken(token: string): Promise<string> {
    // 1. 验证JWT签名合法性
    const decoded = jwt.verify(token, process.env.JWT_SECRET) as { userId: string };
    // 2. 验证用户是否存在(可选,根据业务需求)
    const user = await this.userRepo.findById(decoded.userId);
    if (!user) throw new Error('无效Token');
    return decoded.userId;
  }
}

3. 用 Express 中间件处理 Token 验证

中间件属于接口适配器层,负责把HTTP请求中的Token转换为可用的userId,传递给后续控制器:

// interface-adapters/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AuthorizationPort } from '../../use-cases/ports/authorization.port';

// 通过依赖注入传入授权服务,避免硬编码依赖
export const authMiddleware = (authService: AuthorizationPort) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ message: '未授权' });
    }

    const token = authHeader.split(' ')[1];
    try {
      const userId = await authService.validateToken(token);
      // 给Request扩展userId字段(可通过TypeScript声明合并实现类型支持)
      (req as any).userId = userId;
      next();
    } catch (err) {
      return res.status(401).json({ message: 'Token无效' });
    }
  };
};

4. 实现 CreateBalanceUseCase 与控制器

用例只专注于创建Balance的业务逻辑,不需要处理Token验证:

// use-cases/create-balance.use-case.ts
import { Balance } from '../entities/balance';
import { BalanceRepositoryPort } from './ports/balance-repository.port';

export class CreateBalanceUseCase {
  constructor(private readonly balanceRepo: BalanceRepositoryPort) {}

  async execute(dto: { amount: number; type: 'income' | 'expense'; userId: string }): Promise<Balance> {
    // 业务规则校验:比如金额必须为正
    if (dto.amount <= 0) throw new Error('金额必须大于0');
    // 创建实体
    const balance = new Balance(dto.amount, dto.type, dto.userId);
    // 保存到仓库
    return await this.balanceRepo.save(balance);
  }
}

控制器负责接收HTTP请求,转换为用例需要的DTO:

// interface-adapters/controllers/balance.controller.ts
import { Request, Response } from 'express';
import { CreateBalanceUseCase } from '../../use-cases/create-balance.use-case';

export class BalanceController {
  constructor(private readonly createBalanceUseCase: CreateBalanceUseCase) {}

  async create(req: Request, res: Response) {
    try {
      const { amount, type } = req.body;
      const userId = (req as any).userId;
      const balance = await this.createBalanceUseCase.execute({ amount, type, userId });
      res.status(201).json(balance);
    } catch (err) {
      res.status(400).json({ message: (err as Error).message });
    }
  }
}

5. 依赖注入组装各层

在启动文件中完成实例化和依赖注入,确保所有依赖都是向内的(外层依赖内层抽象):

// frameworks/drivers/index.ts
import express from 'express';
import { BalanceController } from '../../interface-adapters/controllers/balance.controller';
import { CreateBalanceUseCase } from '../../use-cases/create-balance.use-case';
import { PostgresBalanceRepository } from '../../interface-adapters/repositories/postgres-balance.repository';
import { JwtAuthorizationService } from '../../interface-adapters/services/jwt-authorization.service';
import { PostgresUserRepository } from '../../interface-adapters/repositories/postgres-user.repository';
import { authMiddleware } from '../../interface-adapters/middlewares/auth.middleware';

const app = express();
app.use(express.json());

// 实例化数据访问实现
const userRepo = new PostgresUserRepository();
const balanceRepo = new PostgresBalanceRepository();

// 实例化授权服务
const authService = new JwtAuthorizationService(userRepo);

// 实例化用例与控制器
const createBalanceUseCase = new CreateBalanceUseCase(balanceRepo);
const balanceController = new BalanceController(createBalanceUseCase);

// 注册路由,应用授权中间件
app.post('/balances', authMiddleware(authService), (req, res) => balanceController.create(req, res));

app.listen(3000, () => console.log('服务启动在3000端口'));

为什么这样符合 Clean Architecture?

  • 用例层完全不依赖任何外部框架或具体实现,只依赖抽象端口,符合依赖倒置原则
  • 身份验证作为横切关注点,放在接口适配器层的中间件处理,让用例专注于核心业务逻辑,符合单一职责
  • 所有外部依赖(数据库、JWT)都通过抽象接口访问,后续替换授权方式(比如从JWT换OAuth)或数据库,不会影响用例层和实体层

内容的提问来源于stack exchange,提问作者nishi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 18:48:38