一、部署前后端服务
http://doc.ruoyi.vip/ruoyi-vue/
二、现象
若依前后端环境分离版本,本地部署好前后端环境后,访问登录接口密码是明文的,这样显然hi不安全的,如下图所示:
三、解决方法
3.1、加密流程
①、后端生成随机公钥和私钥;
②、前端拿到公钥,集成jsencrypt实现密码加密;
③、前端传输加密后的秘密给后端;
④、后端通过私钥对加密后的秘钥进行解密,验证密码
3.2、若依加密方式
https://doc.ruoyi.vip/ruoyi-vue/document/cjjc.html#%E9%9B%86%E6%88%90jsencrypt%E5%AE%9E%E7%8E%B0%E5%AF%86%E7%A0%81%E5%8A%A0%E5%AF%86%E4%BC%A0%E8%BE%93%E6%96%B9%E5%BC%8F
3.3、后端代码修改
3.3.1、新建RSAUtil
package com.ruoyi.business.utils;
import org.apache.commons.codec.binary.Base64;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
@Component
public class RSAUtil {
// Rsa 私钥 也可固定秘钥对 若依原写法(不安全)
public static String privateKeys = "";
private static String publicKeyStr = "";
private static String privateKeyStr = "";
private static final RSAKeyPair rsaKeyPair = new RSAKeyPair();
/**
* 私钥解密
*
* @param text 待解密的文本
* @return 解密后的文本
*/
public static String decryptByPrivateKey(String text) throws Exception {
return decryptByPrivateKey(rsaKeyPair.getPrivateKey(), text);
}
/**
* 公钥解密
*
* @param publicKeyString 公钥
* @param text 待解密的信息
* @return 解密后的文本
*/
public static String decryptByPublicKey(String publicKeyString, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
/**
* 私钥加密
*
* @param privateKeyString 私钥
* @param text 待加密的信息
* @return 加密后的文本
*/
public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param privateKeyString 私钥
* @param text 待解密的文本
* @return 解密后的文本
*/
public static String decryptByPrivateKey(String privateKeyString, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
/**
* 公钥加密
*
* @param publicKeyString 公钥
* @param text 待加密的文本
* @return 加密后的文本
*/
public static String encryptByPublicKey(String publicKeyString, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 构建RSA密钥对
*
* @return 生成后的公私钥信息
*/
@Bean
public void generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
rsaKeyPair.setPrivateKey(privateKeyString);
rsaKeyPair.setPublicKey(publicKeyString);
publicKeyStr = publicKeyString;
privateKeyStr = privateKeyString;
}
public static String getPublicKey() {
return publicKeyStr;
}
public static String getPrivateKey() {
return privateKeyStr;
}
public static RSAKeyPair rsaKeyPair() {
return rsaKeyPair;
}
/**
* RSA密钥对对象
*/
public static class RSAKeyPair {
private String publicKey;
private String privateKey;
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public RSAKeyPair() {
}
public RSAKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
}
说明:
RSAUtil中添加了@Component注解,generateKeyPair()构建秘钥对添加了@Bean注解,在项目启动时通过@Bean的方式将普通类实例化到Spring容器中,所以当系统启动后,每次调用接口得到的公钥&私钥是一样的,服务重启后,公钥&私钥重新生成。
3.3.2、SysLoginController提供获取公钥接口
/**
* 获取公钥:前端用来密码加密
* @return
*/
@GetMapping("/getPublicKey")
public RSAUtil.RSAKeyPair getPublicKey() {
return RSAUtil.rsaKeyPair();
}
3.3.3、SecurityConfig配置白名单
.antMatchers("/getPublicKey").permitAll()
3.3.4、Postman调用接口获取publicKey
3.3.5、SysLoginService.java#login
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid)
{
// 验证码校验
// validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,RSAUtil.decryptByPrivateKey(password));
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
3.3.6、SysProfileController重置密码接口修改
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(String oldPassword, String newPassword) throws Exception {
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
String userName = loginUser.getUsername();
//加密后的
String password = loginUser.getPassword();
//解密
oldPassword = RSAUtil.decryptByPrivateKey(oldPassword);
newPassword = RSAUtil.decryptByPrivateKey(newPassword);
//拿原密码和加密后的解密
if (!SecurityUtils.matchesPassword(oldPassword, password)) {
return AjaxResult.error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password)) {
return AjaxResult.error("新密码不能与旧密码相同");
}
if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0) {
// 更新缓存用户密码
loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword));
tokenService.setLoginUser(loginUser);
return AjaxResult.success();
}
return AjaxResult.error("修改密码异常,请联系管理员");
}
==========================O(∩_∩)O 后端修改over O(∩_∩)O==========================
3.4、前端代码修改
3.4.1、login.js添加获取公钥的接口
import request from '@/utils/request'
// 获取公钥
export function getPublicKey() {
return request({
url: '/getPublicKey',
method: 'get',
})
}
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
url: '/login',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
// 注册方法
export function register(data) {
return request({
url: '/register',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
// 获取用户详细信息
export function getInfo() {
return request({
url: '/getInfo',
method: 'get'
})
}
// 退出方法
export function logout() {
return request({
url: '/logout',
method: 'post'
})
}
// 获取验证码
export function getCodeImg() {
return request({
url: '/captchaImage',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}
3.4.2、user.js修改登录方法
import { getToken, setToken, removeToken } from '@/utils/auth'
import { login, logout, getInfo, getPublicKey } from '@/api/login'
import {encrypt} from "../../utils/jsencrypt";
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: [],
permissions: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
}
},
actions: {
getPublicKey() {
return new Promise((resolve, reject) => {
getPublicKey()
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
},
// 登录
Login({ commit, dispatch }, userInfo) {
return new Promise((resolve, reject) => {
dispatch('getPublicKey').then(res => {
let publicKey = res.publicKey
const username = userInfo.username.trim()
//调用加密方法(传密码和公钥)
const password = encrypt(userInfo.password, publicKey)
const code = userInfo.code
const uuid = userInfo.uuid
login(username, password, code, uuid)
.then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
})
.catch(error => {
reject(error)
})
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
注意事项:注意上方的导包
3.4.3、修改resetPwd.vue
<template>
<el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">保存</el-button>
<el-button type="danger" size="mini" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { updateUserPwd } from "@/api/system/user";
import { getPublicKey } from '@/api/login';
import { encrypt } from '@/utils/jsencrypt';
export default {
data() {
const equalToPassword = (rule, value, callback) => {
if (this.user.newPassword !== value) {
callback(new Error("两次输入的密码不一致"));
} else {
callback();
}
};
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
// 表单校验
rules: {
oldPassword: [
{ required: true, message: "旧密码不能为空", trigger: "blur" }
],
newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }
],
confirmPassword: [
{ required: true, message: "确认密码不能为空", trigger: "blur" },
{ required: true, validator: equalToPassword, trigger: "blur" }
]
}
};
},
methods: {
getPublicKey() {
return new Promise((resolve, reject) => {
getPublicKey()
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
},
submit() {
this.$refs["form"].validate(valid => {
if (valid) {
this.getPublicKey().then(res=>{
let publicKey = res.publicKey
console.log("res.publicKey",res.publicKey)
const oldPassword = encrypt(this.user.oldPassword, publicKey)
const newPassword = encrypt(this.user.newPassword, publicKey)
updateUserPwd(oldPassword, newPassword).then(
response => {
this.msgSuccess("修改成功");
}
);
})
}
});
},
close() {
this.$tab.closePage();
}
}
};
</script>
3.4.4、jsencrypt.js更改加密方法
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
//这里注掉了原来固定的公私钥
//const publicKey = ''
//const privateKey = ''
// 加密
export function encrypt(txt, publicKey) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据
}
// 解密(暂无使用)
export function decrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
四、启动后端&前端服务,验证登录密码是否加密
五、参考
https://blog.csdn.net/weixin_56567361/article/details/124961493
六、完整代码
链接:https://pan.baidu.com/s/1YKnAcixZGn5zeUkSti1DCA?pwd=yyds
提取码:yyds