springboot+security基于前后端分离的RSA

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

📖摘要

拒绝学习的惰性很可怕

现在与以前不一样,资料多、途径广,在这中间夹杂的广告也非常多。这就让很多初学者很难找到自己要的知识,最后看到有人推荐相关学习资料立刻屏蔽、删除,但同时技术优秀的资料也不能让需要的人看见了。久而久之把更多的时间精力都放在游戏、娱乐、影音上,适当的放松是可以的,但往往沉迷以后就很难出来,因此需要做好一些可以让自己成长的计划,稍有克制。

今天分享下 —— springboot + security基于前后端分离的 RSA 密码加密登录流程,欢迎关注!

重点说明一下关于这个方式请求时可能会出现参数携带+号被置换成空格的骚操作,请移步这篇文章:RSA加密请求报错:javax.crypto.BadPaddingException: Decryption error


🌂RSA加密简介

RSA加密 是一种非对称加密。可以在不直接传递密钥的情况下,完成解密。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。是由一对密钥来进行加解密的过程,分别称为公钥和私钥。两者之间有数学相关,该加密算法的原理就是对一极大整数做因数分解的困难性来保证安全性。通常个人保存私钥,公钥是公开的(可能同时多人持有)。


✨RSA加密

加密是为了安全性考虑,简单的说,加密是为了防止信息被泄露

场景:特工B要给特工A传递一条消息,内容为某一命令。

RSA的加密过程如下:
(1)A特工生成一对密钥(公钥和私钥),私钥不公开,A特工自己保留。公钥为公开的,任何人可以获取。
(2)A特工传递自己的公钥给B特工,B特工用A特工的公钥对消息进行加密。
(3)A特工接收到B特工加密的消息,利用A特工自己的私钥对消息进行解密。

在这个过程中,只有2次传递过程,第一次是A特工传递公钥给B特工,第二次是B特工传递加密消息给A特工,即使都被敌方截获,也没有危险性,因为只有A特工的私钥才能对消息进行解密,防止了消息内容的泄露。基于这一特性,我们可以在前后端登陆上进行密码加密。保证密码的安全性。

image.png


💖RSA密码加密(Java)实现

  • 登录功能,密码肯定不能以明文形式传输,所以前端传过来的密码就应该是RSA加密过后的密码。
  • 因为 RSA 是需要公钥和私钥的,公钥加密,私钥解密。那么就可以随机生成一个公钥私钥密钥对,然后将这个密钥对保存下来,不要泄露,将公钥给前端将密码加密,后端通过私钥解密。最终再使用加盐加密的方法将密码保存到数据库中
  • 下面是 RSA加密 的代码*
  • 实际工作中可以将公钥和私钥提前生成好,然后放到配置文件中去
  • 说一下思路,具体用法可以直接运行 main 主函数慢慢研究。

Base64:封装base64编码。用于RSA密码编码解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
java复制代码import com.fckj.fckjrestaurant.constant.Constant;
import com.fckj.fckjrestaurant.util.codec.Base64Utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;


/**
* @Description: RSA加解密
* @BelongsProject: fckj-restaurant
* @BelongsPackage: com.fckj.fckjrestaurant.util.RSA
* @Author: ChenYongJia
* @CreateTime: 2021-06-04 11:46
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
public class RSAUtils {

/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;

/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;

/**
* 获取密钥对
*
* @return java.security.KeyPair
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static KeyPair getKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance(Constant.ALGORITHM_NAME);
generator.initialize(1024);
return generator.generateKeyPair();
}

/**
* 获取私钥
*
* @param privateKey 私钥字符串
* @return java.security.PrivateKey
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static PrivateKey getPrivateKey(String privateKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
byte[] decodedKey = Base64Utils.decoder(privateKey.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
return keyFactory.generatePrivate(keySpec);
}

/**
* 获取公钥
*
* @param publicKey 公钥字符串
* @param publicKey
* @return java.security.PublicKey
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static PublicKey getPublicKey(String publicKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
byte[] decodedKey = Base64Utils.decoder(publicKey.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
return keyFactory.generatePublic(keySpec);
}

/**
* RSA加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return java.lang.String
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static String encrypt(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(Constant.ALGORITHM_NAME);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int inputLen = data.getBytes().length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
// 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串
// 加密后的字符串
return new String(Base64Utils.encoder(encryptedData));
}

/**
* RSA解密
*
* @param data 待解密数据
* @param privateKey 私钥
* @return java.lang.String
* @date 2021/6/7 15:33
* @author ChenYongJia
* @version 1.0
*/
public static String decrypt(String data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(Constant.ALGORITHM_NAME);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] dataBytes = Base64.decodeBase64(data);
int inputLen = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
//对数据分段解密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataBytes, offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
// 解密后的内容
return new String(decryptedData, "UTF-8");
}

/**
* 签名
*
* @param data 待签名数据
* @param privateKey 私钥
* @return java.lang.String
* @date 2021/6/7 15:33
* @author ChenYongJia
* @version 1.0
*/
public static String sign(String data, PrivateKey privateKey) throws Exception {
byte[] keyBytes = privateKey.getEncoded();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
PrivateKey key = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance(Constant.MD5_RSA);
signature.initSign(key);
signature.update(data.getBytes());
return new String(Base64Utils.encoder(signature.sign()));
}

/**
* 验签
*
* @param srcData 原始字符串
* @param publicKey 公钥
* @param sign 签名
* @return boolean
* @date 2021/6/7 15:33
* @author ChenYongJia
* @version 1.0
*/
public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception {
byte[] keyBytes = publicKey.getEncoded();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
PublicKey key = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(Constant.MD5_RSA);
signature.initVerify(key);
signature.update(srcData.getBytes());
return signature.verify(Base64Utils.decoder(sign.getBytes()));
}

public static void main(String[] args) {
try {
// 生成密钥对
KeyPair keyPair = getKeyPair();
String privateKey = new String(Base64Utils.encoder(keyPair.getPrivate().getEncoded()));
String publicKey = new String(Base64Utils.encoder(keyPair.getPublic().getEncoded()));
log.info("私钥:" + privateKey);
log.info("公钥:" + publicKey);
// RSA加密
String data = "123456";
String encryptData = encrypt(data, getPublicKey(publicKey));
log.info("加密后内容:" + encryptData);
// RSA解密
String decryptData = decrypt(encryptData, getPrivateKey(privateKey));
log.info("解密后内容:" + decryptData);
// RSA签名
String sign = sign(data, getPrivateKey(privateKey));
// RSA验签
boolean result = verify(data, getPublicKey(publicKey), sign);
log.info("验签结果:" + result);
} catch (Exception e) {
e.printStackTrace();
log.error("RSA加解密异常");
}
}

}

Springboot 业务代码实现

逻辑:前端获取 publickey——公钥,用公钥对密码进行编码加密,后端获取密码,用相对应的私钥解密加密数据,与数据库比对密码是否一致

controller 层就不写了,主要看 service 层业务逻辑代码:生成密钥对,放到 redis里面存储,返回前端公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码 @Override
public String getPublicKey() {
try {
Object privateKey = redisUtil.get(Constant.RSA_PRIVATE_KEY);
Object publicKey = redisUtil.get(Constant.RSA_PUBLIC_KEY);
if (WyCheckUtil.isEmpty(publicKey) || WyCheckUtil.isEmpty(privateKey)) {
KeyPair keyPair = RSAUtils.getKeyPair();
privateKey = new String(Base64Utils.encoder(keyPair.getPrivate().getEncoded()));
publicKey = new String(Base64Utils.encoder(keyPair.getPublic().getEncoded()));
// 存入私钥
redisUtil.set(Constant.RSA_PRIVATE_KEY, privateKey);
// 存入公钥
redisUtil.set(Constant.RSA_PUBLIC_KEY, publicKey);
}
log.info("privateKey = {},publicKey = {}", privateKey, publicKey);
return publicKey.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

前端:基于iview和vue框架

1
2
3
4
5
6
javascript复制代码前端用crypto-js进行加密,
npm i jsencrypt,
然后页面头引入import JSEncrypt from 'jsencrypt';
const encrypt = new JSEncrypt();
encrypt.setPublicKey('你的公钥');
password = encrypt.encrypt(‘你的密码’);// 加密后的字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码getPublicKey().then(res => {
let password = this.form.password
let publicKey = res.data.data.data
console.log(publicKey)
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
password = encrypt.encrypt(password)
console.log(password)
this.$emit('on-success-valid', {
userName: this.form.userName,
password: password
})
})

登录验证密码时解密对比

1
2
3
4
5
6
7
8
java复制代码String inputDecryptData = "";
try {
Object privateKey = redisUtil.get(Constant.RSA_PRIVATE_KEY);
inputDecryptData = RSAUtils.decrypt(password, RSAUtils.getPrivateKey(privateKey.toString()));
} catch (Exception e) {
log.error("RSA加解密出现异常======>", e);
throw new BizException("RSA加解密出现异常");
}

先获取私钥,我是放到redis里面,然后用私钥解密加密数据,同事获取数据库用户对象,获取密码同样用私钥解密比对与用户输入的密码是否一致

至此,Springboot&security基于前后端分离的RSA密码加密登录流程就完成了


🎉最后

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%