Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
- 什么是RSA?
- 安全需求介绍
- 前后端交互流程
- 前端使用 RSA 加密密码
- 安装 jsencrypt库
- 实现敏感信息加密
- 服务器端生成RSA的公私钥文件
- Windows环境 生成rsa的公私钥文件
- Linux环境 生成rsa的公私钥文件
- 后端代码实现
- 返回给前端的公钥接口
- 解密前端传入的加密字符串
- 实现效果
什么是RSA?
RSA算法是一种非对称加密算法,与对称加密算法不同的是,RSA算法有两个不同的密钥,一个是公钥,一个是私钥
RSA公开密钥密码体制是一种使用不同的加密密钥与解密密钥,“由已知加密密钥推导出解密密钥在计算上是不可行的”
密码体制
在公开密钥密码体制中,加密密钥(即公开密钥)PK是公开信息,而解密密钥(即秘密密钥)SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,但却不能根据PK计算出SK
正是基于这种理论,1978年出现了著名的RSA算法,它通常是先生成一对RSA密钥,其中之一是保密密钥,由用户保存;另一个为公开密钥,可对外公开,甚至可在网络服务器中注册。为提高保密强度,RSA密钥至少为500位长。这就使加密的计算量很大。为减少计算量,在传送信息时,常采用传统加密方法与公开密钥加密方法相结合的方式,即信息采用改进的DES或IDEA对话密钥加密,然后使用RSA密钥加密对话密钥和信息摘要。对方收到信息后,用不同的密钥解密并可核对信息摘要
安全需求介绍
背景:在安全扫描过程中,系统被检测到存在中危漏洞,影响了用户登录环节的信息安全性
漏洞描述
Web程序在处理用户登录过程中,针对用户名和口令在传输前未采用加密。用户名和口令一旦被恶意攻击者嗅探或暴力破解得到后,可以直接利用它去登录系统。
解决方案:建议对用户名、口令等参数值采用强加密算法,如SHA512、RSA、AES、国密算法等,避免使用MD5、Base64、DES、3DES等弱算法。
问题分析:未加密的敏感信息在传输过程中易被拦截,尤其是在公共网络环境下。这使得系统的认证机制成为攻击目标,存在较高的安全隐患
需求实现:
-
在前端使用安全加密算法对用户名和密码进行加密,防止信息裸露传输
-
后端解密用户信息并进行验证,同时保障传输数据的完整性与安全性
前后端交互流程
基于RSA加密的登录流程,Web端和服务端的交互过程详细步骤:
-
用户发起登录请求:
-
用户在Web端输入登录操作,Web端开始处理登录流程
-
Web端向服务端发送一个请求,要求获取RSA加密所需的公钥
-
-
服务端返回公钥:
- 服务端接收到公钥请求后,从系统中读取公钥信息,并将公钥发送回Web端。这个步骤的响应无需校验
-
Web端加密用户凭据:
-
Web端使用获取到的公钥,对用户的用户名和密码进行加密。加密后的数据用于安全传输,避免敏感信息在网络中被截获
-
Web端将加密后的用户名和密码通过HTTPS协议发送给服务端。HTTPS协议提供了额外的安全层,防止中间人攻击
-
-
服务端解密并验证:
- 服务端接收到加密的数据后,使用私钥对加密内容进行解密。私钥是严格保密的,只在服务端保存和使用
- 服务端对解密后的用户名和密码进行验证,检查凭据是否正确并符合登录要求
-
返回登录结果:
- 服务端验证完成后,将登录验证的结果返回给Web端。结果可以是登录成功或失败的信息
-
Web端处理响应:
- Web端接收并处理服务端返回的登录结果,并显示相应的提示或进行后续操作
前端使用 RSA 加密密码
在前端 Vue.js 中,使用 jsencrypt
库对用户名密码进行加密,并将加密后的用户名密码发送给后端
安装 jsencrypt库
在Vue.js 项目中运行以下命令来安装 jsencrypt
:
npm install jsencrypt --save
实现敏感信息加密
在 Vue.js 登录页面中,对敏感信息进行加密,然后发送给后端:
// 引入 jsencrypt 和 Base64 编码库
import JSEncrypt from 'jsencrypt';
// 需要安装 js-base64 库
import { Base64 } from 'js-base64'; export default {data() {return {username: '', // 用户输入的用户名password: '' // 用户输入的密码};},methods: {/*** 使用 RSA 公钥加密字符串* @param {string} str - 需要加密的字符串* @returns {string|null} - 加密后的字符串,若失败返回 null*/encryptString(str) {// 初始化 JSEncryptconst encrypt = new JSEncrypt();// 设置公钥const publicKey = `-----BEGIN PUBLIC KEY-----...公钥内容...-----END PUBLIC KEY-----`;encrypt.setPublicKey(publicKey);// 加密字符串并返回加密结果return encrypt.encrypt(str);},/*** 用户登录逻辑*/login() {// 获取用户输入的用户名和密码const username = this.username.trim(); // 去除多余空格const password = this.password.trim();// 验证用户名和密码是否为空if (!username || !password) {this.$message.error('用户名或密码不能为空'); // 提示用户return;}// 加密用户名和密码const encryptedUsername = this.encryptString(username);const encryptedPassword = this.encryptString(password);// 验证加密是否成功if (!encryptedUsername || !encryptedPassword) {this.$message.error('加密失败,请检查公钥配置');return;}// Base64 编码的主要作用是将加密后的数据转换成由可见 ASCII 字符组成的字符串,以便在网络传输或存储时避免因特殊字符引发的兼容性问题const base64EncodedUsername = Base64.encode(encryptedUsername);const base64EncodedPassword = Base64.encode(encryptedPassword);// 发送登录请求到后端this.$axios.post('/api/login', {username: base64EncodedUsername, // 加密后的用户名password: base64EncodedPassword // 加密后的密码}).then(response => {// 登录成功后的处理逻辑if (response.data.success) {this.$message.success('登录成功');// 跳转到主页面或其他逻辑} else {this.$message.error(response.data.message || '登录失败');}}).catch(error => {// 请求失败的处理逻辑console.error('登录请求失败:', error);this.$message.error('登录失败,请稍后重试');});}}
};
服务器端生成RSA的公私钥文件
Windows环境 生成rsa的公私钥文件
-
安装 OpenSSL:下载并安装 OpenSSL for Windows,可以从 OpenSSL 官网 下载适合的版本。
-
生成 RSA 私钥:
-
在安装目录的bin目录下面,打开命令提示符(
cmd
) -
运行以下命令生成一个2048位的私钥文件
private_key.pem
:openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
-
-
生成 RSA 公钥:
-
使用生成的私钥来提取公钥,运行以下命令:
openssl rsa -pubout -in private_key.pem -out public_key.pem
-
Linux环境 生成rsa的公私钥文件
使用RSA算法生成pem格式的私钥文件,指定密钥长度2048
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
使用RSA算法在上述私钥文件基础上生成pem格式的公钥文件
openssl rsa -pubout -in private_key.pem -out public_key.pem
执行完成后,会看到输出两份文件 private_key.pem, public_key.pem。
后端代码实现
返回给前端的公钥接口
将生成的两份密钥文件放入指定的目录中,然后在配置文件中配置公私钥文件所属路径
/*** 获取公钥*/@GetMapping("/getPublicKey")public AjaxResult getPublicKey() {// 加载资源File file = new File("公钥的文件所属路径");if (!file.exists()) {throw new BusinessException("未获取到指定公钥文件");}String publicKey;try (InputStream inputStream = Files.newInputStream(file.toPath())) {publicKey = IOUtils.toString(inputStream, StandardCharsets.UTF_8);} catch (IOException e) {log.error("读取公钥文件失败", e);// 返回错误信息return AjaxResultGenerator.error("读取公钥文件失败");}// 检查 publicKey 是否为 null 或空字符串if (publicKey == null || publicKey.isEmpty()) {return AjaxResultGenerator.error("公钥内容为空");}// 删除PEM格式的头部和尾部以及所有空格、换行符publicKey = publicKey.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replaceAll("\\s+", "");return AjaxResultGenerator.success(publicKey);}
解密前端传入的加密字符串
前端的账户和密码进行rsa加密并且base64编码后,调用正常登录接口login()
,readPrivateKeyAndDecode()
方法将 RSA 加密并 Base64 编码的字符串解码后,再用 RSA 私钥对解码后的数据进行解密,最终返回解密后的原始数据
pom.xml
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version>
</dependency>
登录和解密方法
/*** 用户名密码登录** @param loginBody 登录信息* @return 结果*/@PostMapping("/v1/login")public AjaxResult login(@RequestBody LoginBody loginBody) {// 使用 RSA 私钥解密String username = readPrivateKeyAndDecode(loginBody.getUsername());String password = readPrivateKeyAndDecode(loginBody.getPassword());loginBody.setUsername(username);loginBody.setPassword(password);// 执行正常的登录逻辑...return AjaxResultGenerator.success();}/*** 读取私钥并解密** @param rsaEncoded64Str 已rsa加密并且base64编码后的字符串* @return*/public String readPrivateKeyAndDecode(String rsaEncoded64Str) {// 加载资源File file= new File("私钥的文件所属路径");if (!file.exists()) {throw new BusinessException("未获取到指定私钥文件");}String privateKey;try (InputStream inputStream = Files.newInputStream(file.toPath())) {privateKey = IOUtils.toString(inputStream, StandardCharsets.UTF_8);} catch (IOException e) {log.error("读取私钥文件失败", e);throw new BusinessException("读取私钥文件失败:" + e.getMessage());}if (StringUtils.isBlank(privateKey)) {throw new BusinessException("私钥内容为空");}// 删除PEM格式的头部和尾部以及所有空格、换行符privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\s+", "");// 进行base64解密String decodedUsername = new String(Base64.getDecoder().decode(rsaEncoded64Str));// 使用 RSA 私钥解密,用到了hutool-all中的类return new RSA(privateKey, null).decryptStr(decodedUsername, KeyType.PrivateKey);}