在移动应用开发中,用户密码等敏感信息的安全传输至关重要。直接明文传输密码极易被中间人窃取。本文介绍如何利用鸿蒙的 cryptoFramework 及 RSA 公钥加密,实现客户端密码加密、服务端解密的完整登录注册流程。

完整代码:httpDemo。http模块负责网络请求是独立的module,CryptoUtil负责加密。NewLoginPage.ets 演示登录注册加密传输。

一、为什么需要加密登录?

  • 防止窃听:即使使用 HTTPS,部分场景下仍可能被中间人窃取;增加一层应用层加密可增强安全性。
  • 避免明文存储:服务端只存储加密后的密码(或再哈希),但传输过程绝不出现明文。
  • 合规要求:金融、政务等应用必须对敏感数据加密传输。

二、整体流程

  1. 客户端请求公钥:登录页加载时,先调用后端接口获取 RSA 公钥(Base64 编码)。
  2. 前端加密密码:用户输入密码后,使用公钥对密码进行 RSA 加密(PKCS#1 v1.5 填充),得到密文(Base64)。
  3. 发送加密密码:将用户名和加密后的密码发送至登录/注册接口。
  4. 后端解密:服务端使用对应的 RSA 私钥解密,得到明文密码后进行验证或存储(推荐再哈希)。

实际项目开发中需要用哪种加密,需要团队之间协商定义。这里仅作为演示。

三、鸿蒙端代码实现

1. 工具类 CryptoUtil RSA加密相关接口

import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { util } from '@kit.ArkTS';

export class CryptoUtil {
  // ========== 辅助方法 ==========
  private static stringToUint8Array(str: string): Uint8Array {
    const encoder = new util.TextEncoder();
    const buffer = new Uint8Array(str.length * 3);
    const encodeResult = encoder.encodeIntoUint8Array(str, buffer);
    return buffer.slice(0, encodeResult.written);
  }

  private static uint8ArrayToString(data: Uint8Array): string {
    const decoder = util.TextDecoder.create('utf-8');
    return decoder.decodeToString(data);
  }

  // ========== RSA 密钥生成 ==========
  /**
   * 生成 RSA 密钥对(默认 2048 位)
   * @param keyLength 密钥长度(位),默认 2048
   * @returns KeyPair 对象
   */
  static async generateRsaKeyPair(keyLength: number = 2048): Promise<cryptoFramework.KeyPair> {
    try {
      const alg = `RSA${keyLength}`;
      const generator = cryptoFramework.createAsyKeyGenerator(alg);
      return await generator.generateKeyPair();
    } catch (error) {
      console.error(`生成RSA密钥对失败: ${error}`);
      throw new Error(`RSA密钥对生成失败: ${error}`);
    }
  }

  /**
   * 从密钥对获取公钥的 Base64 字符串
   */
  static getPublicKeyBase64(keyPair: cryptoFramework.KeyPair): string {
    try {
      const blob = keyPair.pubKey.getEncoded();
      return new util.Base64Helper().encodeToStringSync(blob.data);
    } catch (error) {
      console.error(`获取公钥Base64失败: ${error}`);
      throw new Error(`获取公钥Base64失败: ${error}`);
    }
  }

  /**
   * 从密钥对获取私钥的 Base64 字符串
   */
  static getPrivateKeyBase64(keyPair: cryptoFramework.KeyPair): string {
    try {
      const blob = keyPair.priKey.getEncoded();
      return new util.Base64Helper().encodeToStringSync(blob.data);
    } catch (error) {
      console.error(`获取私钥Base64失败: ${error}`);
      throw new Error(`获取私钥Base64失败: ${error}`);
    }
  }

  // ========== RSA 公钥导入与加密(客户端登录用) ==========
  /**
   * 导入 RSA 公钥(PKCS#1 格式 Base64)
   * @param pubKeyBase64 公钥的 Base64 字符串(无头尾)
   * @returns PubKey 对象
   */
  static async importRsaPublicKey(pubKeyBase64: string): Promise<cryptoFramework.PubKey> {
    try {
      const keyBlob: cryptoFramework.DataBlob = { data: new util.Base64Helper().decodeSync(pubKeyBase64) };
      const generator = cryptoFramework.createAsyKeyGenerator('RSA2048');
      const keyPair = await generator.convertKey(keyBlob, null);
      return keyPair.pubKey;
    } catch (error) {
      console.error(`导入公钥失败: ${error}`);
      throw new Error('导入RSA公钥失败');
    }
  }

  /**
   * RSA 公钥加密(PKCS1 填充,密钥长度 2048 位)
   * @param plain 明文(长度限制约 245 字节)
   * @param pubKey 公钥
   * @returns Base64 密文
   */
  static async rsaEncrypt(plain: string, pubKey: cryptoFramework.PubKey): Promise<string> {
    try {
      const cipher = cryptoFramework.createCipher('RSA2048|PKCS1');
      await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, pubKey, null);
      const encrypted = await cipher.doFinal({ data: this.stringToUint8Array(plain) });
      return new util.Base64Helper().encodeToStringSync(encrypted.data);
    } catch (error) {
      console.error(`RSA加密失败: ${error}`);
      throw new Error('RSA加密失败');
    }
  }

  // ========== RSA 私钥解密(服务端使用,客户端可选) ==========
  /**
   * RSA 私钥解密(密钥长度 2048 位)
   * @param cipherBase64 Base64 密文
   * @param priKey 私钥
   * @returns 明文
   */
  static async rsaDecrypt(cipherBase64: string, priKey: cryptoFramework.PriKey): Promise<string> {
    try {
      const cipher = cryptoFramework.createCipher('RSA2048|PKCS1');
      await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, priKey, null);
      const decrypted = await cipher.doFinal({ data: new util.Base64Helper().decodeSync(cipherBase64) });
      return this.uint8ArrayToString(decrypted.data);
    } catch (error) {
      console.error(`RSA解密失败: ${error}`);
      throw new Error('RSA解密失败');
    }
  }
}

2. 登录页核心逻辑(使用公钥加密密码)

import { promptAction } from '@kit.ArkUI';
import { AppStorageV2 } from '@kit.ArkUI';
import { APIConstants } from '../common/APIConstants';
import { PublicKeyModel } from '../model/PublicKeyModel';
import { AuthTokenModel } from '../model/AuthTokenModel';
import { LoginRequest } from '../model/LoginRequest';
import { CryptoUtil } from '../utils/CryptoUtil';
import { ApiResponse } from '../model/ApiResponse';
import { httpClient } from '@happy/http';

@Entry
@ComponentV2
struct NewLoginPage {
  @Local isLoginMode: boolean = true;
  @Local username: string = '';
  @Local password: string = '';
  @Local confirmPassword: string = '';
  @Local loading: boolean = false;
  @Local rsaPublicKey: PublicKeyModel = AppStorageV2.connect(PublicKeyModel, () => new PublicKeyModel())!;
  @Local authToken: AuthTokenModel = AppStorageV2.connect(AuthTokenModel, () => new AuthTokenModel())!;

  aboutToAppear() {
    if (!this.rsaPublicKey.value) {
      this.getPublicKey();
    }
  }

  // 获取 RSA 公钥
  private async getPublicKey() {
    if (this.rsaPublicKey.value) return;
    try {
      const resp = await httpClient.get(APIConstants.API_PUBLIC_KEY)
        .execute<ApiResponse<string>>();
      if (resp.status === 200 && resp.data.code === 200 && resp.data.data) {
        this.rsaPublicKey.value = resp.data.data;
        console.log("公钥:" + this.rsaPublicKey.value)
      } else {
        promptAction.showToast({ message: resp.data.message || '获取加密公钥失败' });
      }
    } catch (err) {
      console.error('获取公钥异常', err);
      promptAction.showToast({ message: '网络错误,无法获取公钥' });
    }
  }

  // 登录/注册请求
  private async loginOrRegister(url: string, parameter: LoginRequest): Promise<ApiResponse<AuthTokenModel> | null> {
    try {
      const resp = await httpClient.post(url)
        .json(parameter)
        .execute<ApiResponse<AuthTokenModel>>();
      if (resp.status === 200) return resp.data;
      else {
        promptAction.showToast({ message: resp.data.message || `请求失败 (${resp.status})` });
        return resp.data;
      }
    } catch (err) {
      console.error('请求异常', err);
      promptAction.showToast({ message: '网络连接失败,请稍后重试' });
      return null;
    }
  }

  async handleSubmit() {
    // 表单校验...
    if (!this.username.trim()) { promptAction.showToast({ message: '请输入用户名' }); return; }
    if (!this.password.trim()) { promptAction.showToast({ message: '请输入密码' }); return; }
    if (!this.isLoginMode && this.password !== this.confirmPassword) {
      promptAction.showToast({ message: '两次输入的密码不一致' }); return;
    }
    if (!this.rsaPublicKey.value) {
      promptAction.showToast({ message: '正在获取加密密钥,请稍后再试' }); return;
    }
    if (this.password.length < 6 || this.password.length > 20) {
      promptAction.showToast({ message: '密码长度必须为6-20位' }); return;
    }

    this.loading = true;
    try {
      const pubKey = await CryptoUtil.importRsaPublicKey(this.rsaPublicKey.value);
      const encryptedPassword = await CryptoUtil.rsaEncrypt(this.password.trim(), pubKey);
      console.log("加密后:" + encryptedPassword);
      const url = this.isLoginMode ? APIConstants.API_LOGIN : APIConstants.API_REGISTER;
      const result = await this.loginOrRegister(url, {
        username: this.username.trim(),
        password: encryptedPassword
      });
      if (!result) return;
      if (result.code === 200) {
        if (this.isLoginMode) {
          this.authToken.accessToken = result.data.accessToken;
          this.authToken.refreshToken = result.data.refreshToken;
          this.authToken.userId = result.data.userId;
          promptAction.showToast({ message: '登录成功' });
        } else {
          promptAction.showToast({ message: '注册成功' });
          this.isLoginMode = true;
          this.password = '';
          this.confirmPassword = '';
          this.authToken.accessToken = result.data.accessToken;
          this.authToken.refreshToken = result.data.refreshToken;
          this.authToken.userId = result.data.userId;
        }
      } else {
        promptAction.showToast({ message: result.message });
      }
    } catch (err) {
      console.error('处理异常', err);
      promptAction.showToast({ message: '操作失败,请稍后重试' });
    } finally {
      this.loading = false;
    }
  }

  build() {
    // UI 布局(略)
  }
}

四、效果截图

下面展示本次实践中的三个关键环节截图:

1. 获取公钥接口响应 & 加密后的密码数据

接口返回的 Base64 格式 RSA 公钥(2048位),以及密码经过 RSA 加密后产生的密文(Base64 格式),均在日志中打印。

获取公钥和加密后数据.png

2. 注册成功后数据库存储状态

服务端收到加密密码后,使用私钥解密得到明文,再经哈希处理存入数据库。下图为数据库中的用户记录(密码字段为哈希值)。
注册成功后数据库收到的样子.png

3. 鸿蒙移动端登录成功

服务端收到加密密码后,使用私钥解密得到明文,再经哈希处理对比数据库存储的哈希是否一致,一致则登录成功。

加密登录成功后.png

五、后端处理

后端需要提供两个接口:

1. 获取公钥接口(GET /api/publicKey

返回 Base64 编码的 RSA 公钥(PKCS#1 格式)。注意:私钥需安全存储在服务端(如环境变量或密钥管理服务)。

2. 登录/注册接口(POST /api/login/api/register

  • 接收客户端传来的 encryptedPassword(Base64)。
  • 用 RSA 私钥解密得到明文密码。
  • 验证用户名和密码(如与数据库哈希对比)。
  • 返回 token。

六、注意事项

  1. 密钥长度:建议使用 2048 位 RSA 密钥,1024 位已不够安全。
  2. 填充模式:客户端与服务器必须统一使用 PKCS#1 v1.5 填充(RSA2048|PKCS1)。
  3. 公钥缓存:公钥可缓存在内存中,避免每次请求都获取,但需考虑更新机制(如服务端定期更换密钥对)。
  4. HTTPS 双重保护:RSA 加密不能替代 HTTPS,两者结合更安全。
  5. 密码长度限制:RSA 2048 位最多加密 245 字节(PKCS#1 填充占 11 字节),普通密码完全够用。
  6. 错误处理:网络异常、公钥无效、解密失败等场景需给予明确提示。
  7. 数据库存储:服务端解密得到明文密码后,务必使用强哈希算法(如 bcrypt)加盐存储,绝不要直接存储明文。

七、总结

通过上述方式,我们实现了鸿蒙客户端与后端之间的密码加密传输。关键点在于:

  • 前端:鸿蒙 cryptoFramework 导入公钥并进行 RSA 加密。
  • 后端:提供公钥接口,并持有私钥解密。
  • 安全性:即使网络被监听,也无法直接获取明文密码。

该方案可广泛应用于登录、注册、修改密码等敏感操作。完整代码已集成在项目 CryptoUtil 工具类和登录页中,欢迎参考使用。

如果你有关于鸿蒙加密或安全登录的疑问,欢迎留言交流!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐