在 Vue 3 前端实现登录密码加密传输,核心目标是防止密码以明文形式在网络中传输(避免被中间人窃听,尽管 HTTPS 是基础,但在高安全要求场景下通常需要应用层二次加密)。
最常见的方案是:前端使用公钥加密密码 -> 后端使用私钥解密 。通常采用非对称加密算法 RSA 或 ECC。
以下是基于 Vue 3 + TypeScript + rsaencrypt (或 jsencrypt) 的完整实现方案。
1. 核心流程设计
- 后端生成密钥对:后端生成 RSA 公钥和私钥。
- 获取公钥 :前端在登录页面加载时(或用户点击登录前),调用接口获取后端的公钥(也可以由后端定期轮换,通过接口下发)。
- 前端加密 :用户使用
jsencrypt或node-rsa等库,利用公钥对密码进行加密。 - 发送请求:将加密后的密文(通常是 Base64 字符串)发送给后端。
- 后端解密:后端使用私钥解密密文,得到原始密码,再进行哈希比对(如 BCrypt/Argon2)验证登录。
2. 前置准备
安装加密库。推荐使用 jsencrypt (轻量,专为浏览器设计) 或 encryptlong (如果密码很长,标准 RSA 有长度限制,但密码通常很短,jsencrypt 足够)。
javascript
npm install jsencrypt
# 或者
yarn add jsencrypt
3. Vue 3 代码实现
步骤一:创建加密工具类 (src/utils/rsaEncrypt.ts)
封装加密逻辑,方便复用。
javascript
// src/utils/rsaEncrypt.ts
import JSEncrypt from 'jsencrypt';
// 注意:实际项目中,公钥通常从后端接口动态获取,而不是写死在这里
// 这里为了演示,先假设有一个默认公钥,实际请看步骤二中的动态获取
const DEFAULT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD... (你的后端公钥) ...==
-----END PUBLIC KEY-----`;
/**
* 使用公钥加密字符串
* @param text 待加密的明文(密码)
* @param publicKey 公钥,如果不传则使用默认值(建议动态传入)
*/
export function encryptPassword(text: string, publicKey?: string): string {
const encryptor = new JSEncrypt();
// 设置公钥
encryptor.setPublicKey(publicKey || DEFAULT_PUBLIC_KEY);
// 执行加密
const encrypted = encryptor.encrypt(text);
if (!encrypted) {
throw new Error('密码加密失败');
}
return encrypted;
}
步骤二:在登录组件中使用 (src/views/Login.vue)
在实际场景中,强烈建议在登录页初始化时调用后端接口获取最新的公钥,以防重放攻击或密钥轮换。
javascript
<template>
<div class="login-container">
<h2>用户登录</h2>
<form @submit.prevent="handleLogin">
<div class="form-item">
<label>用户名:</label>
<input v-model="loginForm.username" type="text" required />
</div>
<div class="form-item">
<label>密码:</label>
<!-- 输入框本身可以是 type="password" 隐藏显示,但这不影响传输加密 -->
<input v-model="loginForm.password" type="password" required />
</div>
<button type="submit" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { encryptPassword } from '@/utils/rsaEncrypt';
// 假设你有一个 axios 实例
import request from '@/utils/request';
const loading = ref(false);
const publicKey = ref<string>('');
const loginForm = reactive({
username: '',
password: ''
});
// 1. 页面加载时获取后端公钥
const fetchPublicKey = async () => {
try {
// 模拟接口:/api/auth/public-key
const res = await request.get('/auth/public-key');
// 假设后端返回格式 { code: 200, data: "-----BEGIN PUBLIC KEY-----..." }
publicKey.value = res.data;
console.log('公钥已获取');
} catch (error) {
console.error('获取公钥失败', error);
// 降级策略:如果获取失败,是否允许登录?通常高安场景应禁止
}
};
// 2. 处理登录
const handleLogin = async () => {
if (!loginForm.username || !loginForm.password) {
alert('请输入用户名和密码');
return;
}
if (!publicKey.value) {
alert('系统安全组件未加载,请稍后重试');
return;
}
loading.value = true;
try {
// 【核心步骤】前端加密密码
const encryptedPassword = encryptPassword(loginForm.password, publicKey.value);
// 构造发送数据,注意发送的是加密后的字符串
const payload = {
username: loginForm.username,
password: encryptedPassword,
// 可选:有些方案会带上时间戳或随机盐值防止重放攻击
// timestamp: Date.now()
};
// 发送登录请求
const res = await request.post('/auth/login', payload);
if (res.code === 200) {
alert('登录成功');
// 存储 token 等操作
// localStorage.setItem('token', res.data.token);
} else {
alert(res.message || '登录失败');
}
} catch (error) {
console.error('登录异常', error);
alert('登录失败,请检查网络或联系管理员');
} finally {
loading.value = false;
// 安全起见,加密后立即清空明文密码
loginForm.password = '';
}
};
onMounted(() => {
fetchPublicKey();
});
</script>
<style scoped>
/* 简单样式 */
.login-container { max-width: 400px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
.form-item { margin-bottom: 15px; }
input { width: 100%; padding: 8px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background: #42b983; color: white; border: none; cursor: pointer; }
button:disabled { background: #ccc; }
</style>
4. 后端配合要点 (简述)
前端做了加密,后端必须能解密,否则无法验证。
-
生成密钥对:
- 后端启动时生成一对 RSA 密钥(或使用固定的密钥对存储在配置中心/数据库中)。
- 私钥 :严格保存在后端服务器内存或安全存储中,绝不暴露给前端。
- 公钥:提供给前端接口下载。
-
提供公钥接口:
- 接口示例:
GET /api/auth/public-key - 返回 PEM 格式的公钥字符串。
- 接口示例:
-
登录接口解密:
- 接收前端传来的
encryptedPassword。 - 使用私钥进行解密,得到原始密码
rawPassword。 - 重要 :拿到
rawPassword后,不要直接跟数据库里的明文比(数据库里存的应该是加盐哈希值,如 BCrypt)。 - 流程:
解密得到明文->使用同样的哈希算法(如BCrypt)计算哈希->与数据库哈希值比对。
- 接收前端传来的