Clean Architecture下Express+Node.js余额创建的用户认证实现疑问
Clean Architecture 下的 Token 验证与 Balance 创建最佳实践
你并没有误解 Clean Architecture,核心问题是要理清各层的职责边界,通过抽象端口+依赖倒置来解决分层依赖的冲突。下面是具体的实践方案:
首先明确分层边界(Clean Architecture 核心)
- 实体层:Balance 核心业务实体,只包含业务规则,不依赖任何外部层
- 用例层:
CreateBalanceUseCase,负责核心业务逻辑,仅依赖实体和抽象端口(输入/输出端口),不碰任何具体实现 - 接口适配器层:控制器、中间件、服务实现、数据访问适配器,负责把外部系统(HTTP、数据库)的格式转换为用例层能识别的抽象接口
- 框架驱动层: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
相关产品推荐
相关产品推荐

