Skip to content

当然!我们来探讨安全领域一个绝对无法绕开的话题:哈希与加密 (Hashing and Encryption)。这就像是为你的应用配备了一套“保险箱”和“密码本”,确保即使数据被盗,窃贼也无法读懂其中的内容。

为你的数据配备“保险箱”与“密码本”:NestJS 哈希与加密指南

想象你的应用是一家银行。

银行里存放着两类至关重要的信息:

  1. 客户的取款密码:这是用来验证身份的。
  2. 客户的身份证号、家庭住址:这是客户的敏感隐私数据

对于这两类信息,我们的安保策略是截然不同的:

对于“取款密码” -> 使用哈希 (Hashing),就像一个“单向打碎机”

  • 目标:我们只需要验证客户输入的密码是否正确,但永远、永远不需要知道密码原文是什么。我们绝不能明文存储用户的密码!
  • 工作原理:当客户设置密码“123456”时,我们不存储“123456”,而是把它扔进一个特殊的“单向打碎机”(哈希函数,如 bcrypt)。这台机器会把它打成一串毫无规律的、固定长度的乱码,比如 $2b$10$K/d...
  • 特性
    • 单向性:你无法从这串乱码 $2b$10$K/d... 反推出原始密码“123456”。就像你无法从一堆玻璃碎片复原出一个完整的杯子。
    • 唯一性:同样的输入“123456”,每次经过打碎机,得到的乱码都是不同的(因为哈希函数会加入一个随机的“盐”salt),但打碎机内部有办法验证它们都源自同一个原文。
  • 验证过程:当客户再次输入密码“123456”时,我们把这个新输入的密码和数据库里存的那串乱码一起扔进打碎机的“验证口”。打碎机会告诉我们“是的,它们匹配”或“不,它们不匹配”。

对于“敏感数据” -> 使用加密 (Encryption),就像一个“带钥匙的保险箱”

  • 目标:我们需要安全地存储客户的身份证号,并且在需要的时候(比如办理业务时)能够取出来查看原文
  • 工作原理:我们把身份证号“123...”放进一个“保险箱”(加密算法,如 AES),然后用一把秘密密钥 (secret key) 把它锁上。锁上后,保险箱里只是一串乱码。
  • 特性
    • 双向性:只要你有正确的密钥,你就可以打开保险箱,将里面的乱码解密 (decrypt) 回原始的身份证号。没有密钥,它就是一堆废铁。

NestJS 作为一个后端框架,本身不提供具体的加密或哈希实现,但它可以非常轻松地集成 Node.js 生态中经过安全审计的、最优秀的库。

1. 哈希 (Hashing):安全地处理用户密码

行业标准:使用 bcrypt 库。它专为密码哈希设计,内置了加盐(salting)和可配置的工作因子(cost factor),能有效抵抗彩虹表攻击和暴力破解。

第一步:安装依赖

bash
npm install bcrypt
# 安装类型定义文件
npm install -D @types/bcrypt

第二步:在用户服务中实现哈希逻辑

我们通常会在 UsersService 的创建用户逻辑中,对密码进行哈希处理。

src/users/users.service.ts

typescript
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  private readonly users = []; // 模拟数据库

  // 1. 创建用户时,哈希密码
  async create(createUserDto: CreateUserDto) {
    // a. 定义工作因子 (salt rounds)。数值越高,哈希越慢,但也越安全。10-12 是一个很好的起点。
    const saltOrRounds = 10;

    // b. 使用 bcrypt.hash() 进行哈希
    const hashedPassword = await bcrypt.hash(
      createUserDto.password,
      saltOrRounds
    );

    const newUser = {
      username: createUserDto.username,
      password: hashedPassword, // 在数据库中存储哈希后的密码
    };

    this.users.push(newUser);
    console.log('新用户已创建,存储的密码是:', hashedPassword);

    // 永远不要返回密码哈希给客户端
    const { password, ...result } = newUser;
    return result;
  }

  // ...
}

第三步:在认证服务中比较密码

AuthService 中,我们需要验证用户登录时输入的密码。

src/auth/auth.service.ts

typescript
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);

    if (user) {
      // 使用 bcrypt.compare() 来比较明文密码和哈希值
      const isMatch = await bcrypt.compare(pass, user.password);

      if (isMatch) {
        const { password, ...result } = user;
        return result;
      }
    }
    return null;
  }
}```
`bcrypt.compare()` 会自动处理盐值等复杂细节,你只需要提供明文和哈希值即可。这是唯一正确的密码比较方式。

### 2. 加密 (Encryption):保护敏感数据

**行业标准**:使用 Node.js 内置的 **`crypto`** 模块,采用 **AES-256-GCM** 算法。

#### **前置知识:AES-256-GCM**
*   **AES**: 一种**对称加密**标准,意味着加密和解密使用**同一个密钥**。
*   **256**: 指的是密钥的长度是 256 位,这是非常安全的级别。
*   **GCM**: 一种操作模式,它不仅提供了加密,还提供了**认证 (Authenticated Encryption)**。这意味着它能保证数据不仅是保密的,而且在传输过程中**没有被篡改**过。对于网络通信,这至关重要。

**挑战**:直接使用 `crypto` 模块的 API 比较底层和复杂,需要手动管理**密钥 (key)**、**初始化向量 (IV)** 和**认证标签 (auth tag)**。

我们可以封装一个 `CryptoService` 来简化这个过程。

**第一步:创建一个可注入的加密服务**

**`src/crypto/crypto.service.ts`**
```typescript
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';

@Injectable()
export class CryptoService {
  private readonly key: Buffer;
  private readonly algorithm = 'aes-256-gcm';

  constructor(private readonly configService: ConfigService) {
    // 关键!加密密钥必须是 32 字节 (256 位)
    // 绝不能硬编码,必须从安全的环境变量中获取
    const secret = this.configService.get<string>('ENCRYPTION_SECRET');
    this.key = Buffer.from(secret, 'hex'); // 假设密钥是 64 位的十六进制字符串
  }

  encrypt(text: string): string {
    const iv = crypto.randomBytes(16); // 生成一个随机的初始化向量
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);

    const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
    const authTag = cipher.getAuthTag();

    // 将 iv, authTag 和加密内容组合在一起存储,解密时需要它们
    return Buffer.concat([iv, authTag, encrypted]).toString('hex');
  }

  decrypt(encryptedText: string): string {
    const data = Buffer.from(encryptedText, 'hex');

    const iv = data.slice(0, 16);
    const authTag = data.slice(16, 32);
    const encrypted = data.slice(32);

    const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
    decipher.setAuthTag(authTag);

    const decrypted = Buffer.concat([decipher.update(encrypted, 'hex', 'utf8'), decipher.final('utf8')]);

    return decrypted.toString();
  }
}

第二步:在服务中使用它

typescript
// users.service.ts
@Injectable()
export class UsersService {
  constructor(private readonly cryptoService: CryptoService) {}

  async create(dto: CreateUserDto) {
    // ... 哈希密码 ...

    // 加密用户的身份证号
    const encryptedIdCard = this.cryptoService.encrypt(dto.idCardNumber);

    const newUser = {
      // ...
      idCard: encryptedIdCard, // 在数据库中存储加密后的值
    };

    // ...
  }

  async findOne(username: string) {
    const userFromDb = /* ...从数据库获取用户... */;

    // 需要时,解密数据
    const decryptedIdCard = this.cryptoService.decrypt(userFromDb.idCard);

    return {
      ...userFromDb,
      idCard: decryptedIdCard, // 返回解密后的明文
    };
  }
}

3. 中国企业级方案的思考

上述方案在技术上是稳固的,但在中国的企业级应用和金融级应用中,对安全的要求会更加严苛,并会引入更复杂的体系。

痛点与演进:

  1. 密钥管理 (Key Management): ENCRYPTION_SECRET 怎么安全地存储和分发?直接写在 .env 文件中,对于高安全级别的应用是不可接受的。
  2. 数据脱敏 (Data Masking): 在很多场景下(如客服查询、数据分析),我们不希望操作人员看到完整的敏感信息,而是希望看到脱敏后的数据,比如 138****1234张*三
  3. 国密算法支持: 在金融、政府等领域,法律法规强制要求使用国家商用密码算法(如 SM2, SM3, SM4)。

企业级方案 Demo:集成硬件加密机 (HSM) / 密钥管理系统 (KMS) + 数据脱敏网关

  • 密钥管理:

    • 方案: 企业会使用硬件加密机 (HSM)云厂商提供的密钥管理服务 (KMS)(如 AWS KMS, 阿里云 KMS)来生成、存储和轮换密钥。
    • 流程:
      1. CryptoService 不再自己持有密钥。
      2. 当需要加密时,CryptoService 会调用 KMS 的 API,将明文数据发送给 KMS。
      3. KMS 在其安全的硬件内部进行加密,然后只返回密文。密钥本身永远不会离开 KMS。
      4. 解密时,将密文发送给 KMS,KMS 返回明文
    • 优点: 实现了密钥和应用逻辑的物理隔离,安全性达到最高级别。
  • 数据脱敏:

    • 方案: 脱敏逻辑通常不会写在每个 Service 中,而是通过一个统一的“数据脱敏网关”自定义的序列化装饰器来实现。
    • @Mask() 装饰器伪代码:
      typescript
      // a custom decorator for serialization
      @Transform(({ value, obj }) => {
        const currentUser = /* ...get current user from context... */;
        if (currentUser.hasPermission('view_full_phone_number')) {
          return value;
        }
        return value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
      })
      phone: string;
      在序列化(返回给客户端)时,这个装饰器会检查当前用户的权限,动态决定是返回完整电话号码还是脱敏后的号码。这使得权限控制和数据展现逻辑紧密结合,且易于管理。
  • 国密算法:

    • 方案: 使用实现了国密算法的第三方库(如 gm-sm4)来替换 crypto 模块中的 AES 实现。CryptoService 的封装接口保持不变,只需更换底层的加密引擎即可。

总结

哈希和加密是数据安全的左右护法,它们用途不同,绝不能混淆。

目的技术核心库/算法特性银行比喻
验证身份 (密码)哈希 (Hashing)bcrypt单向,不可逆单向打碎机
保护数据 (隐私信息)加密 (Encryption)crypto (AES-256-GCM)双向,可用密钥解密带钥匙的保险箱
  • 基础实践: 对密码使用 bcrypt 进行哈希;对敏感数据,封装一个 CryptoService 来使用 crypto 模块进行加解密。
  • 企业级实践: 将密钥管理委托给 KMS,并通过统一的网关或装饰器实现灵活的数据脱敏策略,并根据合规要求选择国密算法

牢记“密码用哈希,数据用加密”的黄金法则,是构建任何一个值得信赖的应用程序的起点。