news 2026/6/18 16:38:56

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙应用中的安全登录:RSA加密传输密码实践

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

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

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

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

二、整体流程

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

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

三、鸿蒙端代码实现

1. 工具类CryptoUtilRSA加密相关接口

import{cryptoFramework}from'@kit.CryptoArchitectureKit';import{util}from'@kit.ArkTS';exportclassCryptoUtil{// ========== 辅助方法 ==========privatestaticstringToUint8Array(str:string):Uint8Array{constencoder=newutil.TextEncoder();constbuffer=newUint8Array(str.length*3);constencodeResult=encoder.encodeIntoUint8Array(str,buffer);returnbuffer.slice(0,encodeResult.written);}privatestaticuint8ArrayToString(data:Uint8Array):string{constdecoder=util.TextDecoder.create('utf-8');returndecoder.decodeToString(data);}// ========== RSA 密钥生成 ==========/** * 生成 RSA 密钥对(默认 2048 位) * @param keyLength 密钥长度(位),默认 2048 * @returns KeyPair 对象 */staticasyncgenerateRsaKeyPair(keyLength:number=2048):Promise<cryptoFramework.KeyPair>{try{constalg=`RSA${keyLength}`;constgenerator=cryptoFramework.createAsyKeyGenerator(alg);returnawaitgenerator.generateKeyPair();}catch(error){console.error(`生成RSA密钥对失败:${error}`);thrownewError(`RSA密钥对生成失败:${error}`);}}/** * 从密钥对获取公钥的 Base64 字符串 */staticgetPublicKeyBase64(keyPair:cryptoFramework.KeyPair):string{try{constblob=keyPair.pubKey.getEncoded();returnnewutil.Base64Helper().encodeToStringSync(blob.data);}catch(error){console.error(`获取公钥Base64失败:${error}`);thrownewError(`获取公钥Base64失败:${error}`);}}/** * 从密钥对获取私钥的 Base64 字符串 */staticgetPrivateKeyBase64(keyPair:cryptoFramework.KeyPair):string{try{constblob=keyPair.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):Promise<cryptoFramework.PubKey>{try{constkeyBlob:cryptoFramework.DataBlob={data:newutil.Base64Helper().decodeSync(pubKeyBase64)};constgenerator=cryptoFramework.createAsyKeyGenerator('RSA2048');constkeyPair=awaitgenerator.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):Promise<string>{try{constcipher=cryptoFramework.createCipher('RSA2048|PKCS1');awaitcipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE,pubKey,null);constencrypted=awaitcipher.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):Promise<string>{try{constcipher=cryptoFramework.createCipher('RSA2048|PKCS1');awaitcipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE,priKey,null);constdecrypted=awaitcipher.doFinal({data:newutil.Base64Helper().decodeSync(cipherBase64)});returnthis.uint8ArrayToString(decrypted.data);}catch(error){console.error(`RSA解密失败:${error}`);thrownewError('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@ComponentV2struct NewLoginPage{@LocalisLoginMode:boolean=true;@Localusername:string='';@Localpassword:string='';@LocalconfirmPassword:string='';@Localloading:boolean=false;@LocalrsaPublicKey:PublicKeyModel=AppStorageV2.connect(PublicKeyModel,()=>newPublicKeyModel())!;@LocalauthToken:AuthTokenModel=AppStorageV2.connect(AuthTokenModel,()=>newAuthTokenModel())!;aboutToAppear(){if(!this.rsaPublicKey.value){this.getPublicKey();}}// 获取 RSA 公钥privateasyncgetPublicKey(){if(this.rsaPublicKey.value)return;try{constresp=awaithttpClient.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:'网络错误,无法获取公钥'});}}// 登录/注册请求privateasyncloginOrRegister(url:string,parameter:LoginRequest):Promise<ApiResponse<AuthTokenModel>|null>{try{constresp=awaithttpClient.post(url).json(parameter).execute<ApiResponse<AuthTokenModel>>();if(resp.status===200)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.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{constpubKey=awaitCryptoUtil.importRsaPublicKey(this.rsaPublicKey.value);constencryptedPassword=awaitCryptoUtil.rsaEncrypt(this.password.trim(),pubKey);console.log("加密后:"+encryptedPassword);consturl=this.isLoginMode?APIConstants.API_LOGIN:APIConstants.API_REGISTER;constresult=awaitthis.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。

六、注意事项

  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工具类和登录页中,欢迎参考使用。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/18 16:29:09

缓存之道:拆分、复用与80/20法则

一、一个贯穿计算机系统的通用思想如果你仔细观察计算机系统中的各种优化手段&#xff0c;会发现一个反复出现的模式&#xff1a;把操作中「不可复用的大块」&#xff0c;拆成粒度合适的「可复用小块」&#xff0c;将频繁使用的小块缓存起来供后续请求命中复用。这个模式出现在…

作者头像 李华
网站建设 2026/6/18 16:26:28

Python全栈修炼之路 | 第20篇 :元类与Python对象模型深度解析

系列导读&#xff1a;本系列面向有一定Python基础的开发者&#xff0c;深入讲解Python高级特性与工程实践。建议按顺序阅读&#xff0c;每篇包含完整知识体系、底层原理剖析与实战项目。 引言&#xff1a;为什么元类是Python对象模型的终极关卡 在Python中&#xff0c;"一…

作者头像 李华
网站建设 2026/6/18 16:18:33

深入解析wd-v1-4-moat-tagger-v2.csv:AI图像自动标注工作流的核心映射文件

1. 项目概述&#xff1a;从文件名到图像标注工作流如果你在AI绘画、图像生成或者内容审核的圈子里混过一段时间&#xff0c;大概率见过或者用过一些“标签器”&#xff08;Tagger&#xff09;。这些工具能自动分析一张图片&#xff0c;然后给你输出一长串描述性的英文标签&…

作者头像 李华
网站建设 2026/6/18 16:02:44

Python自动化抢票终极指南:3步掌握DamaiHelper实战技巧

Python自动化抢票终极指南&#xff1a;3步掌握DamaiHelper实战技巧 【免费下载链接】DamaiHelper 大麦网演唱会演出抢票脚本。 项目地址: https://gitcode.com/gh_mirrors/dama/DamaiHelper 还在为抢不到心仪的演唱会门票而烦恼吗&#xff1f;DamaiHelper是一款基于Pyth…

作者头像 李华
网站建设 2026/6/18 16:00:13

emWin Flex皮肤系统深度解析:从结构体到主题管理的嵌入式GUI定制实战

1. 项目概述与核心价值在嵌入式GUI开发领域&#xff0c;尤其是资源受限的MCU平台上&#xff0c;界面的美观度和交互体验往往与产品竞争力直接挂钩。很多开发者都曾面临这样的困境&#xff1a;使用原生控件&#xff0c;界面显得千篇一律&#xff0c;缺乏品牌特色&#xff1b;而想…

作者头像 李华