鸿蒙应用中的安全登录:RSA加密传输密码实践

📅 2026/6/18 16:39:10
鸿蒙应用中的安全登录:RSA加密传输密码实践
在移动应用开发中用户密码等敏感信息的安全传输至关重要。直接明文传输密码极易被中间人窃取。本文介绍如何利用鸿蒙的cryptoFramework及 RSA 公钥加密实现客户端密码加密、服务端解密的完整登录注册流程。完整代码httpDemo。http模块负责网络请求是独立的moduleCryptoUtil负责加密。NewLoginPage.ets 演示登录注册加密传输。一、为什么需要加密登录防止窃听即使使用 HTTPS部分场景下仍可能被中间人窃取增加一层应用层加密可增强安全性。避免明文存储服务端只存储加密后的密码或再哈希但传输过程绝不出现明文。合规要求金融、政务等应用必须对敏感数据加密传输。二、整体流程客户端请求公钥登录页加载时先调用后端接口获取 RSA 公钥Base64 编码。前端加密密码用户输入密码后使用公钥对密码进行 RSA 加密PKCS#1 v1.5 填充得到密文Base64。发送加密密码将用户名和加密后的密码发送至登录/注册接口。后端解密服务端使用对应的 RSA 私钥解密得到明文密码后进行验证或存储推荐再哈希。实际项目开发中需要用哪种加密需要团队之间协商定义。这里仅作为演示。三、鸿蒙端代码实现1. 工具类CryptoUtilRSA加密相关接口import{cryptoFramework}fromkit.CryptoArchitectureKit;import{util}fromkit.ArkTS;exportclassCryptoUtil{// 辅助方法 privatestaticstringToUint8Array(str:string):Uint8Array{constencodernewutil.TextEncoder();constbuffernewUint8Array(str.length*3);constencodeResultencoder.encodeIntoUint8Array(str,buffer);returnbuffer.slice(0,encodeResult.written);}privatestaticuint8ArrayToString(data:Uint8Array):string{constdecoderutil.TextDecoder.create(utf-8);returndecoder.decodeToString(data);}// RSA 密钥生成 /** * 生成 RSA 密钥对默认 2048 位 * param keyLength 密钥长度位默认 2048 * returns KeyPair 对象 */staticasyncgenerateRsaKeyPair(keyLength:number2048):PromisecryptoFramework.KeyPair{try{constalgRSA${keyLength};constgeneratorcryptoFramework.createAsyKeyGenerator(alg);returnawaitgenerator.generateKeyPair();}catch(error){console.error(生成RSA密钥对失败:${error});thrownewError(RSA密钥对生成失败:${error});}}/** * 从密钥对获取公钥的 Base64 字符串 */staticgetPublicKeyBase64(keyPair:cryptoFramework.KeyPair):string{try{constblobkeyPair.pubKey.getEncoded();returnnewutil.Base64Helper().encodeToStringSync(blob.data);}catch(error){console.error(获取公钥Base64失败:${error});thrownewError(获取公钥Base64失败:${error});}}/** * 从密钥对获取私钥的 Base64 字符串 */staticgetPrivateKeyBase64(keyPair:cryptoFramework.KeyPair):string{try{constblobkeyPair.priKey.getEncoded();returnnewutil.Base64Helper().encodeToStringSync(blob.data);}catch(error){console.error(获取私钥Base64失败:${error});thrownewError(获取私钥Base64失败:${error});}}// RSA 公钥导入与加密客户端登录用 /** * 导入 RSA 公钥PKCS#1 格式 Base64 * param pubKeyBase64 公钥的 Base64 字符串无头尾 * returns PubKey 对象 */staticasyncimportRsaPublicKey(pubKeyBase64:string):PromisecryptoFramework.PubKey{try{constkeyBlob:cryptoFramework.DataBlob{data:newutil.Base64Helper().decodeSync(pubKeyBase64)};constgeneratorcryptoFramework.createAsyKeyGenerator(RSA2048);constkeyPairawaitgenerator.convertKey(keyBlob,null);returnkeyPair.pubKey;}catch(error){console.error(导入公钥失败:${error});thrownewError(导入RSA公钥失败);}}/** * RSA 公钥加密PKCS1 填充密钥长度 2048 位 * param plain 明文长度限制约 245 字节 * param pubKey 公钥 * returns Base64 密文 */staticasyncrsaEncrypt(plain:string,pubKey:cryptoFramework.PubKey):Promisestring{try{constciphercryptoFramework.createCipher(RSA2048|PKCS1);awaitcipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE,pubKey,null);constencryptedawaitcipher.doFinal({data:this.stringToUint8Array(plain)});returnnewutil.Base64Helper().encodeToStringSync(encrypted.data);}catch(error){console.error(RSA加密失败:${error});thrownewError(RSA加密失败);}}// RSA 私钥解密服务端使用客户端可选 /** * RSA 私钥解密密钥长度 2048 位 * param cipherBase64 Base64 密文 * param priKey 私钥 * returns 明文 */staticasyncrsaDecrypt(cipherBase64:string,priKey:cryptoFramework.PriKey):Promisestring{try{constciphercryptoFramework.createCipher(RSA2048|PKCS1);awaitcipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE,priKey,null);constdecryptedawaitcipher.doFinal({data:newutil.Base64Helper().decodeSync(cipherBase64)});returnthis.uint8ArrayToString(decrypted.data);}catch(error){console.error(RSA解密失败:${error});thrownewError(RSA解密失败);}}}2. 登录页核心逻辑使用公钥加密密码import{promptAction}fromkit.ArkUI;import{AppStorageV2}fromkit.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}fromhappy/http;EntryComponentV2struct NewLoginPage{LocalisLoginMode:booleantrue;Localusername:string;Localpassword:string;LocalconfirmPassword:string;Localloading:booleanfalse;LocalrsaPublicKey:PublicKeyModelAppStorageV2.connect(PublicKeyModel,()newPublicKeyModel())!;LocalauthToken:AuthTokenModelAppStorageV2.connect(AuthTokenModel,()newAuthTokenModel())!;aboutToAppear(){if(!this.rsaPublicKey.value){this.getPublicKey();}}// 获取 RSA 公钥privateasyncgetPublicKey(){if(this.rsaPublicKey.value)return;try{constrespawaithttpClient.get(APIConstants.API_PUBLIC_KEY).executeApiResponsestring();if(resp.status200resp.data.code200resp.data.data){this.rsaPublicKey.valueresp.data.data;console.log(公钥this.rsaPublicKey.value)}else{promptAction.showToast({message:resp.data.message||获取加密公钥失败});}}catch(err){console.error(获取公钥异常,err);promptAction.showToast({message:网络错误无法获取公钥});}}// 登录/注册请求privateasyncloginOrRegister(url:string,parameter:LoginRequest):PromiseApiResponseAuthTokenModel|null{try{constrespawaithttpClient.post(url).json(parameter).executeApiResponseAuthTokenModel();if(resp.status200)returnresp.data;else{promptAction.showToast({message:resp.data.message||请求失败 (${resp.status})});returnresp.data;}}catch(err){console.error(请求异常,err);promptAction.showToast({message:网络连接失败请稍后重试});returnnull;}}asynchandleSubmit(){// 表单校验...if(!this.username.trim()){promptAction.showToast({message:请输入用户名});return;}if(!this.password.trim()){promptAction.showToast({message:请输入密码});return;}if(!this.isLoginModethis.password!this.confirmPassword){promptAction.showToast({message:两次输入的密码不一致});return;}if(!this.rsaPublicKey.value){promptAction.showToast({message:正在获取加密密钥请稍后再试});return;}if(this.password.length6||this.password.length20){promptAction.showToast({message:密码长度必须为6-20位});return;}this.loadingtrue;try{constpubKeyawaitCryptoUtil.importRsaPublicKey(this.rsaPublicKey.value);constencryptedPasswordawaitCryptoUtil.rsaEncrypt(this.password.trim(),pubKey);console.log(加密后encryptedPassword);consturlthis.isLoginMode?APIConstants.API_LOGIN:APIConstants.API_REGISTER;constresultawaitthis.loginOrRegister(url,{username:this.username.trim(),password:encryptedPassword});if(!result)return;if(result.code200){if(this.isLoginMode){this.authToken.accessTokenresult.data.accessToken;this.authToken.refreshTokenresult.data.refreshToken;this.authToken.userIdresult.data.userId;promptAction.showToast({message:登录成功});}else{promptAction.showToast({message:注册成功});this.isLoginModetrue;this.password;this.confirmPassword;this.authToken.accessTokenresult.data.accessToken;this.authToken.refreshTokenresult.data.refreshToken;this.authToken.userIdresult.data.userId;}}else{promptAction.showToast({message:result.message});}}catch(err){console.error(处理异常,err);promptAction.showToast({message:操作失败请稍后重试});}finally{this.loadingfalse;}}build(){// UI 布局略}}四、效果截图下面展示本次实践中的三个关键环节截图1. 获取公钥接口响应 加密后的密码数据接口返回的 Base64 格式 RSA 公钥2048位以及密码经过 RSA 加密后产生的密文Base64 格式均在日志中打印。2. 注册成功后数据库存储状态服务端收到加密密码后使用私钥解密得到明文再经哈希处理存入数据库。下图为数据库中的用户记录密码字段为哈希值。3. 鸿蒙移动端登录成功服务端收到加密密码后使用私钥解密得到明文再经哈希处理对比数据库存储的哈希是否一致一致则登录成功。五、后端处理后端需要提供两个接口1. 获取公钥接口GET/api/publicKey返回 Base64 编码的 RSA 公钥PKCS#1 格式。注意私钥需安全存储在服务端如环境变量或密钥管理服务。2. 登录/注册接口POST/api/login或/api/register接收客户端传来的encryptedPasswordBase64。用 RSA 私钥解密得到明文密码。验证用户名和密码如与数据库哈希对比。返回 token。六、注意事项密钥长度建议使用2048 位RSA 密钥1024 位已不够安全。填充模式客户端与服务器必须统一使用PKCS#1 v1.5填充RSA2048|PKCS1。公钥缓存公钥可缓存在内存中避免每次请求都获取但需考虑更新机制如服务端定期更换密钥对。HTTPS 双重保护RSA 加密不能替代 HTTPS两者结合更安全。密码长度限制RSA 2048 位最多加密245 字节PKCS#1 填充占 11 字节普通密码完全够用。错误处理网络异常、公钥无效、解密失败等场景需给予明确提示。数据库存储服务端解密得到明文密码后务必使用强哈希算法如 bcrypt加盐存储绝不要直接存储明文。七、总结通过上述方式我们实现了鸿蒙客户端与后端之间的密码加密传输。关键点在于前端鸿蒙cryptoFramework导入公钥并进行 RSA 加密。后端提供公钥接口并持有私钥解密。安全性即使网络被监听也无法直接获取明文密码。该方案可广泛应用于登录、注册、修改密码等敏感操作。完整代码已集成在项目CryptoUtil工具类和登录页中欢迎参考使用。如果你有关于鸿蒙加密或安全登录的疑问欢迎留言交流