目 录CONTENT

文章目录

使用 RSA 非对称加密前后端交互的敏感数据

Hello!你好!我是村望~!
2025-06-29 / 0 评论 / 0 点赞 / 91 阅读 / 3,465 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

简单了解RSA非对称加密

https://www.veritas.com/zh/cn/information-center/rsa-encryption

目前,存在两种主要类型的加密:

  • 对称加密使用相同的密钥来加密和解密数据,例如高级加密标准 (AES)
  • 非对称加密也称为公钥加密,因为它需要一对密钥,公钥用于加密,私钥用于解密。Rivest Shamir Adleman 算法就是一个常见的例子。

RSA 之所以有效,是因为随机选择的足够长度的加密密钥几乎是坚不可摧的。

它也称为非对称算法,其中发送者和接收者使用不同的密钥来加密和解密数据。非对称算法为每个发送方分配一对密钥:

  • 用于加密的公用密钥
  • 用于解密数据的私人密钥

尽管这两个密钥是链接的,但无法从公共密钥导出私有密钥或使用公共密钥解密数据。顾名思义,公钥是众所周知的,但私钥是秘密的,只有拥有它们的用户才能使用。简而言之,每个人都可以使用其公钥向用户发送消息,但只有预期的接收者可以使用其私钥解密消息。

Nest中实现前后端交互数据的RSA加密

步骤

  • 首先生成公钥和私钥
    • 公钥存放在前端
    • 私钥存放在后端
  • 前端传递敏感信息的时候通过公钥进行加密
  • 后端通过私钥进行解密

生成公钥私钥

你可以通过命令或者一些在线网站都可以!

openssl genpkey -aes256 -algorithm RSA \
  -out encrypted_private.pem \
  -pkeyopt rsa_keygen_bits:4096
  1. genpkey
    OpenSSL 的通用密钥生成命令(替代旧的 genrsa
  2. -aes256
    • 使用 AES-256 算法加密私钥文件
    • 执行命令后会提示输入密码(用于加密私钥)
    • 后续使用该私钥时需要提供此密码
  3. -algorithm RSA
    指定生成 RSA 密钥对
  4. -out encrypted_private.pem
    输出文件名(PEM 格式)
  5. -pkeyopt rsa_keygen_bits:4096
    密钥选项:设置 RSA 密钥长度为 4096 位 [现代推荐至少 2048 位(3072/4096 位更安全)]

ok. 我们在本地的项目生成一下看看

➜  nest-project git:(rbac-nest) ✗ openssl genpkey -aes256 -algorithm RSA \
  -out encrypted_private.pem \
  -pkeyopt rsa_keygen_bits:4096
..+...+.........+............+.+........+.+......+..+..........+..+...+............+......+.......+......+...........+++++++++++++++++++++++++++++++++++++++++++++*.+......+.....+.........+....+...+........+............+...+....+...............+..+......+...+.+......+...+.....+.+...+.................+...+......+....+.....+...+....+......+...+..............+.+++++++++++++++++++++++++++++++++++++++++++++*.....+.+.........+...+.....+...+.+...+..+....+............+............+..+...+...............+...+.........................+.....+...+....+...........+........................+...+............+...+.............+..+.........+..........+.....+....+..+.........+.........+....+...............+..+......+.......+......+............+...+...........+.+.........+.....+.+...+............+..+..........+......+..........................+......+....+...+............+........+.+.................+...............+...+.+...+++++
....+.................+......+....+......+++++++++++++++++++++++++++++++++++++++++++++*.............+++++++++++++++++++++++++++++++++++++++++++++*...+.........+.....+.......+......+..............+.+........+....+........+...................+.........+..+.......+...+...............+...............+..............+......+....+..............+....+....................+.+..+...+.........+..........+...+...+...........+......+...+...................+......+..+............+.+.....................+.........+..+.........+.+........+......+.............+.....+......+........................+.+.........+.....+.......+.........+......+........................+....................+.+......+............+.....+...+.........+...................+..+....+.....+..........+...........+.......+........+...+.+...+......+...........+...+......................+...........+....+...............+......+..+.+..............+................+.........+....................+......+.+...+...+...+.....+.+........................+........................+...+.....+...+......+.+...+......+...+..+...+...+...............+...+....+...+...+.....+......+...+.............+..+............+.+........+..............................+.........+......+.......+..............+.+.....+.........+........................+......+...+...+.........+...+.......+...+...+..+.......+........+......+......+....+..+.......+..+......+.........+....+...............+...........+....+.........+......+..+....+...+......+............+...+..+...+.......+........+.+..........................+..................+.+........+.+++++
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

PEM pass phrase 是 test

使用私钥提取公钥

openssl rsa -pubout -in encrypted_private.pem -out public_key.pem 
参数 说明
rsa RSA 密钥处理命令
-pubout 要求输出公钥(默认输出私钥)
-in private_key.pem 输入的私钥文件
-out public_key.pem 输出的公钥文件路径
nest-project git:(rbac-nest) ✗ openssl rsa -pubout -in encrypted_private.pem -out public_key.pem 

Enter pass phrase for encrypted_private.pem: # 输入刚刚设置的 PEM pass phrase : test
writing RSA key
➜  nest-project git:(rbac-nest) ✗ 

就可以看到当前执行目录下生成了公钥文件!

业务场景:用户的注册和登录

创建项目什么的就不在这讲了(什么初始化项目,数据库配置)

我这里使用的是 Typeorm + Mysql

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { User } from './user/entities/user.entity';
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '123',
      database: 'rsa-test',
      synchronize: true,
      logging: true,
      entities: [User],
      poolSize: 10,
      connectorPackage: 'mysql2',
      extra: {
        authPlugin: 'sha256_password',
      },
    }),
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

生成用户 resource

nest g resource user

UserEntity

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    length: 50,
  })
  username: string;

  @Column({
    length: 100,
  })
  password: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;
}

用户 CreateUserDto, 简单,只要用户密码

export class CreateUserDto {
  username: string;
  password: string;
}

创建用户的 controller 没啥逻辑,就调用就完了!一切从简,也不做什么字段校验了!

@Post()
create(@Body() createUserDto: CreateUserDto) {
  return this.userService.create(createUserDto);
}

首先我们先来实现 注册的 Service

注册的用户入库前,密码也是要做加密的!

node 项目一切加密相关的可以用 bcrypt 这个包

npm i bcrypt 

added 3 packages, removed 14 packages, and audited 768 packages in 5s

133 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

还有对应类型的包

npm i @types/bcrypt       

added 1 package, and audited 769 packages in 2s

133 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

UserService 实现

  // 创建用户
  async create(createUserDto: CreateUserDto) {
    // 1.首先看数据库里有没有 username 已经存在的
    const findByUsernameRet = await this.userRepository.findOne({
      where: {
        username: createUserDto.username,
      },
    });
    if (findByUsernameRet) {
      return '用户已经注册过了';
    }
    // 2. 没有则创建用户
    const newUser = new User();
    newUser.username = createUserDto.username;
    newUser.password = await bcrypt.hash(createUserDto.password, 10);
    return this.userRepository.save(newUser);
  }

测试一下

curl --location --request POST 'http://localhost:3000/user' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: localhost:3000' \
--header 'Connection: keep-alive' \
--data-raw '{
    "username":"nor_user",
    "password":"123"
}'
id username password createTime updateTime
1 nor_user $2b$10$71pv1UE9e9F3.lS5AQK88.OvUlRK7cpOtTcVPWT7mRRtEXw3lGQfm 2025-06-29 03:07:40.372160 2025-06-29 03:07:40.372160

这一步其实还没用到我们的 RSA ,因为还没前端我们设计的是前端的传输都是通过 RSA 加密后的信息

简单的实现下前端先!

   <main class="max-w-4xl mx-auto px-4 pt-16 pb-12">
      <div
        class="bg-white/70 backdrop-blur-sm rounded-[2.5rem] shadow-2xl p-12"
      >
        <div class="grid">
          <div>
            <div class="mb-10 text-center">
              <img
                src="https://unpkg.com/lucide-static@latest/icons/lock.svg"
                class="mx-auto w-12 h-12 p-2 bg-primary-100 rounded-xl mb-4"
              />
              <h2 class="text-2xl font-bold text-gray-800">账户登录</h2>
            </div>

            <div class="space-y-6">
              <!-- 输入框组 -->
              <div>
                <label class="block text-sm font-medium text-gray-700 mb-2"
                  >电子邮箱</label
                >
                <div class="relative">
                  <input
                    type="username"
                    v-model="loginForm.username"
                    class="w-full outline-none bg-white/80 backdrop-blur-sm border-2 border-gray-200/50 rounded-xl py-3 px-4 focus:border-primary-600 focus:ring-1 focus:ring-primary-600 transition"
                    placeholder="name@example.com"
                  />
                  <img
                    src="https://unpkg.com/lucide-static@latest/icons/mail.svg"
                    class="absolute right-4 top-3 w-5 h-5 text-gray-400"
                  />
                </div>
              </div>

              <div>
                <label class="block text-sm font-medium text-gray-700 mb-2"
                  >密码</label
                >
                <div class="relative">
                  <input
                    type="password"
                    v-model="loginForm.password"
                    class="w-full outline-none bg-white/80 backdrop-blur-sm border-2 border-gray-200/50 rounded-xl py-3 px-4 focus:border-primary-600 focus:ring-1 focus:ring-primary-600 transition"
                    placeholder="••••••••"
                  />
                  <img
                    src="https://unpkg.com/lucide-static@latest/icons/lock.svg"
                    class="absolute right-4 top-3 w-5 h-5 text-gray-400"
                  />
                </div>
              </div>
              <div class="flex justify-center gap-10">
                <n-button color="#8a2be2" @click="loginAction">
                  <template #icon>
                    <n-icon>
                      <LogIn />
                    </n-icon>
                  </template>
                  登录
                </n-button>
                <n-button color="#f00" @click="registerAction">
                  <template #icon>
                    <n-icon>
                      <AddOutline />
                    </n-icon>
                  </template>
                  立即注册
                </n-button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>

image-20250629135103045

registerAction 这个方法的实现

// 点击注册
const registerAction = async () => {
  const loginParams = Object.assign({}, loginForm);
  loginParams.password = encrypted(loginParams.password);
  const res = await userRegister(loginParams);
  message.info(res.data);
};

我们需要把密码通过 encrypted 方法(内部实现 RSA 加密)处理后,传给后端

下面 encrypted 函数就是使用生成的公钥进行加密具体实现

npm install node-forge --save
npm install @types/node-forge --save-dev

这里我们使用 node-forge 这个库在前端处理 RSA 加密

import forge from 'node-forge';

// 公钥内容
const publicKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnZZFfQ5s2Qk1XLhRqXwG
+Oo9jwfqrr+t+48aDmBh8sDlG6Fl5k6iVgsRJ7o6cURQnqZCVaoVs2lGDlN87Qak
lH3VzlkfwwBbFeAOQA2eav+BgaT9u1P46MYB1li4iUdSyrCOcSOob8aUp3lZ14N3
OpwHtPk+IL0WIRzhwVnlDF8Xr8/gLvz+D71mVBdaNm1Zw2vq8K6PJzVV8z+7CFJo
ZNuShCpNhVYafxSeblg6OPObQnzWCa0eCb96v6GsiID0bKjX2hKW8cekjl1xWhd+
kmH/tvtkRTcRMUdWR+yAkzAIox3+ZpWrwas2hcIgBxuFA0miicd2Ek6gk0f6kSyr
iwli2daeJ645mUDMvusCR/c6UHVqUQ6g9cFmksj7RzTAHAwjrq6y35/Q/vrgv6Q3
QUND9xLuSx9eG/2zmf+47MZiaE3am7teShZOLsH4r421/+n8q38z1uJPcQuqVHjy
7lOVmYkOuSpANoaeu6FIMUAeo9lmRSubN9ZIqK8iapC41oaKlrTw/JgViu1eabz0
tGfHF+GZgvohq7PDza741hwnbBGTFeV1vdFj7AUgqjmXMVkGQ48PC8kg2xInio7F
p6X/JQ8nEMa5F740lzdeppcIYsMBcBisMZnpwwXkiyOmXQV1PK1ICTH/IKSuI7ud
4YKymGrw6KfO8ZYavPnXbzECAwEAAQ==
-----END PUBLIC KEY-----`;

/**
 * 使用OAEP填充模式加密字符串
 * @param str 要加密的字符串
 * @returns 加密后的base64字符串
 */
export const encrypted = (str: string) => {
  try {
    // 需要安装node-forge库
    // npm install node-forge --save
    // npm install @types/node-forge --save-dev
    const publicKeyForge = forge.pki.publicKeyFromPem(publicKey);
    const encrypted = publicKeyForge.encrypt(str, 'RSA-OAEP', {
      md: forge.md.sha1.create(),
      mgf1: {
        md: forge.md.sha1.create(),
      },
    });

    // 转换为base64格式
    const base64Result = forge.util.encode64(encrypted);
    return base64Result;
  } catch (e) {
    console.error('加密失败:', e);
    return '';
  }
};

我们试一下发起注册请求看看是什么样子

curl 'http://localhost:5173/api/user' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Language: zh-CN,zh;q=0.9' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsInVzZXJuYW1lIjoiY2h0ZXN0NSIsImlhdCI6MTc0MjYwOTkyNCwiZXhwIjoxNzQyNjk2MzI0fQ.kE6y2wItnRyXNWYWfXzzSQq2SNrP_xpi6_23quCpSOI' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: application/json;charset=UTF-8' \
  -b 'session=.eJy9kdtum0AQht-F61DtLiyG3sXHYAWccDTbVtEewAYW4sRYsany7l3cg5S0aaWq6sUuO8PMfDPzf9Ye72W-195_-KzR3e6uFNr78wMAqF2cXy1tcuX8eMC5Y348jDCyh19D3p0s990592yJfM8fS_YtmlnWEA2L78Hn2gwjVECAdTsXlm5aAukMCqCDwnBGjmCMIvt7wjfyvjuIvO2054tfcSyMnaE3YL_gcIEZog7VGR8h3bQx0G0OuG4ykRsOLTil9BWnyynf5o9vcUYGHW572AGjUN0mUBNiE78kj2yGLAcCnRU2000E1YQGVrPiAhbMxtwwilfkYc1UNGWrPX8a6K-UwG8rgf9eCWjQEQOs0C0TAd0cQVunkFs6NBHl0IQjVrB_ocSfOfQ1538pAZHxOyU-XWjdfZ23yp-fllu24OWqXLpx70K_dPduG2A-cS233q2TydJ5p4Ia3iQ9VU7SzPccxXu3kVuhbC_i0IsujVVUP_nlU0nbYOdW92XWeJBEY5lFWbdK_do_AUAW8-Y62spV6nZev638amb4KMar6Qa7KldcyScSKnC7lEQdjjbl9WTZi9QdGmxv4_lYnQmDm6No5NRHMsuM-JjLcSZaeVCUWECBkyYoMyMomLFsqCTSX5PWm8BOpDMQxRyliXMTguzI4k0XVRvsR3VPQbLKU7wLYCL5bN7TKlgFKSEkTcIY-TdsTR7COAj8NBmnMTG96fw2vKq7yJiZERK3UUP61XoHqZzfeNCn_my7EGtxXKPjTThRS21wScqf95UZ_mnwu1UGvT6oyNQF12lSen3d-QsXeSdYkugSXEc1JI1fZZV38qbbyguHmonJf2gwQ37lnWuyRlYUOocshXLQYn2lJujj49fvfLBV_5eDDUk1b9wWvHvA49Ta3vZ9tdwlD57ZTtrwrt4AJ4wXBm6w2ItNXxRXvL_Xnr8AdPK1MQ.aF9b4g.CNZEX3pNToMVlJnMGtL0rDXTpes' \
  -H 'Origin: http://localhost:5173' \
  -H 'Referer: http://localhost:5173/login' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-origin' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '{"username":"i933310737@qq.com","password":"FlY6/9gQDGbmbkinWJ12o7IFnv+RmlzCVI5SPVo5h62AeKmcJS0QSUFnah4LTDfPmyNIysGBfA1Y/qd3l0l/BYf8CNxeqLBS+eGVNxP2Iauh+2uYYSpgXbPIm8GxJCbJ6aeIre1lb9OhyRMNkcxOqOcoUrdXZJY3O6Uzr/7ShwbBa166DTyG4YcIFGzeI7gFslJSye7pIQhfvXlLFh6s3271GRu70GqPmw8YyWMC2ITKKp4UVmgX7ZnVWvlHJ0FJmnG+VOVIrykF3ULZSaTNPb/P7ETEHLC1RsJt/PVEcmqMeBvU7VY+GCUNvVtEV8Qq20jksHge0kI5Qgcj2qz3DLHwmHXzyBPnbw57FYb3vQTmtMwv0K18iJ7E5xyVLMWn2VF63w0intoR5k1eRArldOMiONBMdtRkVVGfctn9E4x3a7Gxsa9x38HcXVmr26Pdgs4m0YZEttqXCg2WyoDqUIMjR8st9/as02LG7q269lrLlvhllZdhY8d6BP0jcZZDzX3x2b+W+KfVxDnAGIUVM3Y368qKCTioZtT+EkxY1ccYEcOqrFQ7UBpnYyY3s34vYKi5/0iDpcRQcI5k/2pFCLSP/5TjS2TxfZNw4FZX+JrvsJeU5fQtRf64QqnHMnGG/Stpvd9fhjurSSXL20prSkMsXBfgUIeVD/eH6zYmLY8="}'

可以看到前端的 password 字段已经被加密了

后端私钥解密为明文:

上面我们实现的注册,是直接把前端明文传过来的 password 进行 hash 加密,因为前端传输加了 RSA 的加密过程,所以我们接收到password 后需要先进行 RSA 解密为明文后,再hash 加密入库

生成 RSA module 和 Service

nest g module rsa  
CREATE rsa/rsa.module.ts (80 bytes)

nest g service rsa 
CREATE rsa/rsa.service.spec.ts (439 bytes)
CREATE rsa/rsa.service.ts (87 bytes)
UPDATE rsa/rsa.module.ts (151 bytes)

我们先把 公钥私钥放在 assets [这里记得打包 nest 项目也要把 asset 的内部打包进去,因为只会打包 src 目录下的内容]

image-20250629143314389

image-20250629143133113

RsaServuce 的实现

import { Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import { readFileSync } from 'fs';
import * as path from 'path';

@Injectable()
export class RsaService {
  private privateKey: string;

  constructor() {
    // 读取私钥
    const privateKeyPath = path.join(
      process.cwd(),
      'assets/encrypted_private.pem',
    );
    this.privateKey = readFileSync(privateKeyPath, 'utf-8');
    console.log('privateKey', this.privateKey);
  }

  //decrypt 解密函数
  decrypt(encryptedText: string): string {
    console.log({ encryptedText });
    const buffer = Buffer.from(encryptedText, 'base64');
    const decrypted = crypto.privateDecrypt(
      {
        key: this.privateKey,
        passphrase: 'test', // 这是我们通过 openssl 生成私钥的时候设置的
        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, // 使用 PKCS#1 填充
      },
      buffer,
    );
    return decrypted.toString('utf8');
  }
}

RsaModule providers 把 RsaServcie 暴露给IOC exports 把 RsaServcie 可以给其他模块注入

import { Module } from '@nestjs/common';
import { RsaService } from './rsa.service';

@Module({
  providers: [RsaService],
  exports: [RsaService],
})
export class RsaModule {}

AppModule 可以不用加,这里没有需要注册 Controller

引入 RsaModule

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { RsaModule } from '../rsa/rsa.module';

@Module({
  imports: [TypeOrmModule.forFeature([User]), RsaModule],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService], // Export UserService to be used in other modules
})
export class UserModule {}

UserService

// 创建用户
async create(createUserDto: CreateUserDto) {
  // 1.首先看数据库里有没有 username 已经存在的
  const findByUsernameRet = await this.userRepository.findOne({
    where: {
      username: createUserDto.username,
    },
  });
  if (findByUsernameRet) {
    return '用户已经注册过了';
  }
  // 2. 没有则创建用户
  const newUser = new User();
  newUser.username = createUserDto.username;
  const rsaParsePassword = this.rsaSerivce.decrypt(createUserDto.password);
  console.log(rsaParsePassword);
  newUser.password = await bcrypt.hash(rsaParsePassword, 10);
  return await this.userRepository.save(newUser);
}

注册请求下

image-20250629140834464
curl 'http://localhost:5173/api/user' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Language: zh-CN,zh;q=0.9' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsInVzZXJuYW1lIjoiY2h0ZXN0NSIsImlhdCI6MTc0MjYwOTkyNCwiZXhwIjoxNzQyNjk2MzI0fQ.kE6y2wItnRyXNWYWfXzzSQq2SNrP_xpi6_23quCpSOI' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: application/json;charset=UTF-8' \
  -b 'session=.eJy9kdtum0AQht-F61DtLiyG3sXHYAWccDTbVtEewAYW4sRYsany7l3cg5S0aaWq6sUuO8PMfDPzf9Ye72W-195_-KzR3e6uFNr78wMAqF2cXy1tcuX8eMC5Y348jDCyh19D3p0s990592yJfM8fS_YtmlnWEA2L78Hn2gwjVECAdTsXlm5aAukMCqCDwnBGjmCMIvt7wjfyvjuIvO2054tfcSyMnaE3YL_gcIEZog7VGR8h3bQx0G0OuG4ykRsOLTil9BWnyynf5o9vcUYGHW572AGjUN0mUBNiE78kj2yGLAcCnRU2000E1YQGVrPiAhbMxtwwilfkYc1UNGWrPX8a6K-UwG8rgf9eCWjQEQOs0C0TAd0cQVunkFs6NBHl0IQjVrB_ocSfOfQ1538pAZHxOyU-XWjdfZ23yp-fllu24OWqXLpx70K_dPduG2A-cS233q2TydJ5p4Ia3iQ9VU7SzPccxXu3kVuhbC_i0IsujVVUP_nlU0nbYOdW92XWeJBEY5lFWbdK_do_AUAW8-Y62spV6nZev638amb4KMar6Qa7KldcyScSKnC7lEQdjjbl9WTZi9QdGmxv4_lYnQmDm6No5NRHMsuM-JjLcSZaeVCUWECBkyYoMyMomLFsqCTSX5PWm8BOpDMQxRyliXMTguzI4k0XVRvsR3VPQbLKU7wLYCL5bN7TKlgFKSEkTcIY-TdsTR7COAj8NBmnMTG96fw2vKq7yJiZERK3UUP61XoHqZzfeNCn_my7EGtxXKPjTThRS21wScqf95UZ_mnwu1UGvT6oyNQF12lSen3d-QsXeSdYkugSXEc1JI1fZZV38qbbyguHmonJf2gwQ37lnWuyRlYUOocshXLQYn2lJujj49fvfLBV_5eDDUk1b9wWvHvA49Ta3vZ9tdwlD57ZTtrwrt4AJ4wXBm6w2ItNXxRXvL_Xnr8AdPK1MQ.aF9b4g.CNZEX3pNToMVlJnMGtL0rDXTpes' \
  -H 'Origin: http://localhost:5173' \
  -H 'Referer: http://localhost:5173/login' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-origin' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '{"username":"i933310737@qq.com","password":"kZm4IWzbpJAllrI9bq721F8QiaYgvnqlqfOoUeen3LRoIxGWmEOmYXfIirbIV1/rkO6bRyKcCV0EEq8EHvlvw4B5LNDO8kbu2gg/l9yhytckMHeMkoz7wa1FyNNbpDxUnm7+ZCVM9H8W+f21F40ZxgA+hLilWo8tmt3euDPEy1yfeNl4PRBWH0uTaRylOYKvq8frfZsSP4WuxKoS4t5ED4y7r7u8jC8/olQgLqGh6p0zGsgv/eOO1IyvjryDAdvt6StT86QlqsepxHlor5slAzk6nej5tChBj2hYJnKn9M0O8pqRuKySnWSM0fivf754qW1nI5pkYJOWqid76DDeFtOwcpaxyrh8JtKP+duaD/tnHszOVSTJIm+dwGRtM4mSJ/tVU4AUvxnd9tWGkbv/PiyH5/uqBw7TI5U2BpFPhr0kFUZRwjMJ3Kzyhda4U2zmI/fcwuHbodCqZqg69p5L9AmMRPczSX+zPe/uj1eIzplS53PmAvs5LQ+K1zYhLdnFR81CV4Iw0T2OJAhnSQe7x49cgkOatdvLRt32OrhZLQS7+xAOUxdZeqfAm+wrtsggMmeFud2G/BIydF0WdHXWhOFXLULK/9A2gYXSIigJeK1wycvmRJrsN3ItCg8OJTwjotrS6rrQfrB5Rv2pRHamcP3hcYFoQG3EfvPn7OKvXuY="}'

可以看到请求解析的已经成功了!

image-20250629140804740

后面就按照之前的正常加密入库就可以了

实现登录

后端部分

LoginUserDto

import { IsNotEmpty, Length } from 'class-validator';

export class LoginUserDto {
  @IsNotEmpty()
  @Length(1, 20)
  username: string;

  @IsNotEmpty()
  @Length(1, 1000)
  password: string;
}

Login Handler

  @Post('/login')
  async login(@Body() loginUserDto: LoginUserDto) {
    return this.userService.login(loginUserDto);
  }

login Service

// 用户登录
async login(loginUserDto: CreateUserDto) {
  const findByUsernameRet = await this.userRepository.findOne({
    where: {
      username: loginUserDto.username,
    },
  });
  if (!findByUsernameRet) {
    return '用户不存在';
  }
  const rsaParsePassword = this.rsaSerivce.decrypt(loginUserDto.password);
  console.log(rsaParsePassword);
  const isMatch = await bcrypt.compare(
    rsaParsePassword,
    findByUsernameRet.password,
  );
  if (!isMatch) {
    return '账号或密码错误';
  }
  return '用户登录成功';
}

前端部分

// userLogin 用户登录
export const userLogin = (data: any) => fetchPost('/user/login', data);

// 用户登录方法
const loginAction = async () => {
  const loginParams = Object.assign({}, loginForm);
  loginParams.password = encrypted(loginParams.password);
  const res = await userLogin(loginParams);
  message.info(res.data);
};

登录接口实现

测试一下 我们刚刚注册的账号是 i933310737@qq.com/eerr123ss

iShot_2025-06-29_14.28.34

那这里就完成了 前后端的数据通过 RSA 进行了加密的过程!

流程

  1. 客户端用 RSA 公钥加密密码
  2. 服务器用 RSA 私钥解密
  3. 服务器用 bcrypt 哈希解密后的密码并存储

其他的注意点

  • 定期更换私钥密码是重要的安全实践
0

评论区