鸿蒙应用中的安全登录:RSA加密传输密码实践
·
在移动应用开发中,用户密码等敏感信息的安全传输至关重要。直接明文传输密码极易被中间人窃取。本文介绍如何利用鸿蒙的 cryptoFramework 及 RSA 公钥加密,实现客户端密码加密、服务端解密的完整登录注册流程。
完整代码:httpDemo。http模块负责网络请求是独立的module,CryptoUtil负责加密。NewLoginPage.ets 演示登录注册加密传输。
一、为什么需要加密登录?
- 防止窃听:即使使用 HTTPS,部分场景下仍可能被中间人窃取;增加一层应用层加密可增强安全性。
- 避免明文存储:服务端只存储加密后的密码(或再哈希),但传输过程绝不出现明文。
- 合规要求:金融、政务等应用必须对敏感数据加密传输。
二、整体流程
- 客户端请求公钥:登录页加载时,先调用后端接口获取 RSA 公钥(Base64 编码)。
- 前端加密密码:用户输入密码后,使用公钥对密码进行 RSA 加密(PKCS#1 v1.5 填充),得到密文(Base64)。
- 发送加密密码:将用户名和加密后的密码发送至登录/注册接口。
- 后端解密:服务端使用对应的 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 格式),均在日志中打印。

2. 注册成功后数据库存储状态
服务端收到加密密码后,使用私钥解密得到明文,再经哈希处理存入数据库。下图为数据库中的用户记录(密码字段为哈希值)。
3. 鸿蒙移动端登录成功
服务端收到加密密码后,使用私钥解密得到明文,再经哈希处理对比数据库存储的哈希是否一致,一致则登录成功。

五、后端处理
后端需要提供两个接口:
1. 获取公钥接口(GET /api/publicKey)
返回 Base64 编码的 RSA 公钥(PKCS#1 格式)。注意:私钥需安全存储在服务端(如环境变量或密钥管理服务)。
2. 登录/注册接口(POST /api/login 或 /api/register)
- 接收客户端传来的
encryptedPassword(Base64)。 - 用 RSA 私钥解密得到明文密码。
- 验证用户名和密码(如与数据库哈希对比)。
- 返回 token。
六、注意事项
- 密钥长度:建议使用 2048 位 RSA 密钥,1024 位已不够安全。
- 填充模式:客户端与服务器必须统一使用 PKCS#1 v1.5 填充(
RSA2048|PKCS1)。 - 公钥缓存:公钥可缓存在内存中,避免每次请求都获取,但需考虑更新机制(如服务端定期更换密钥对)。
- HTTPS 双重保护:RSA 加密不能替代 HTTPS,两者结合更安全。
- 密码长度限制:RSA 2048 位最多加密 245 字节(PKCS#1 填充占 11 字节),普通密码完全够用。
- 错误处理:网络异常、公钥无效、解密失败等场景需给予明确提示。
- 数据库存储:服务端解密得到明文密码后,务必使用强哈希算法(如 bcrypt)加盐存储,绝不要直接存储明文。
七、总结
通过上述方式,我们实现了鸿蒙客户端与后端之间的密码加密传输。关键点在于:
- 前端:鸿蒙
cryptoFramework导入公钥并进行 RSA 加密。 - 后端:提供公钥接口,并持有私钥解密。
- 安全性:即使网络被监听,也无法直接获取明文密码。
该方案可广泛应用于登录、注册、修改密码等敏感操作。完整代码已集成在项目 CryptoUtil 工具类和登录页中,欢迎参考使用。
如果你有关于鸿蒙加密或安全登录的疑问,欢迎留言交流!
更多推荐

所有评论(0)