一、技术背景介绍
- 后端:Java+Spring Boot + Spring Security + JWT
- 前端:Ant Design Pro
二、Web登录安全隐患
如果你的后台管理系统登录情况和下图的情况一样,登录时用户名、密码是明文传输的,那么你是时候考虑要用非对称性加密来保障Web登录安全了。
三、HTTPS就一定安全吗?
HTTPS存在两种可能的风险:
1、HTTPS可以保证传输过程中的信息不被别人截获,但是细细思考下,HTTPS是应用层协议,下层采用SSL保证信息安全,但是在客户端和服务端,密文同样是可以被截获的;
2、HTTPS报文在传输过程中,如果客户端被恶意引导安装“中间人”的WEB信任证书,那么HTTPS中的“中间人攻击”一样会将明文密码泄露给别人。
结论是:无论HTTP还是HTTPS,密码必须密文传输。
四、采用什么方式才能保障安全
4.1 Base64转码可以吗?
有同学可能会考虑,现在登录名、密码是明文不安全,那我将用户名、密码机进行Base64转码别人不就看不懂了吗?这个同学有点天真有点可爱,因为Base64完全没有安全性可言,你能用Base64转码,别人就能用Base64解码,而且不用写代码,百度一下Base64在线解码就可以了,所以这种方案不可行。
4.2 对称加密可以吗?
答案仍然是:不行。稍微有点技术能力的人,只要利用相关工具或手段,就能看到你前端所采用的对称加密算法和密钥,分分钟破解,所以这种方式仍然不够安全。
4.3 MD5可以吗?
有同学考虑用MD5方案的原因大概是认为MD5是不可逆的,所以转码后,即使“黑客”拿到了密文那也是是非常难破解的(前提是密码不是常用密码,如:admin、123456这种,不然就有可能被暴力破解或者密码库进行破解)。问题是,你前端MD5转码后传到后端,后端无法进行解码,所以也不认识你传的是个什么鬼,不能进行下一步的相关逻辑处理,所以这种方案仍然不可行。
五、非对称性加密可以很好地解决这个问题
对于还不了解什么是非对称性加密的同学,可以先看看非对称性加密的基础知识点。
5.1 非对称性加密定义
1976年,美国学者Dime和Henman为解决信息公开传送和密钥管理问题,提出一种新的密钥交换协议,允许在不安全的媒体上的通讯双方交换信息,安全地达成一致的密钥,这就是“公开密钥系统”。
5.2 非对称性加密简介
- 非对称加密算法又称现代加密算法。
- 非对称加密是计算机通信安全的基石,保证了加密数据不会被破解。
- 与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey) 和私有密(privatekey)
- 公开密钥和私有密钥是一对
- 如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密。
- 如果用私有密钥对数据进行加密,只有用对应的公开密钥才能解密。
- 因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
即:
1 、A与B要通信,A和B都要产生一对用于加密和解密的公钥和私钥
2、A的私钥保密,A的公钥告诉B;B的私钥保密,B的公钥告诉A。
3、A要给B发送信息时,A用B的公钥加密信息,因为A知道B的公钥。
4、A将这个消息发给B(已经用B的公钥加密消息)。
5、B收到这个消息后,B用自己的私钥解密A的消息。其他所有收到这个报文的人都无法解密,因为只有B才有B的私钥。
5.3 非对称加密经典算法
RSA:可用于加密和签名
DSA:仅用于签名,但速度更快
5.4 特点
算法强度复杂,安全性依赖于算法与密钥。
5.5缺点
由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。了解完非对称性加密的内容后,我们可以发现,用非对称性加密就能很好的解决Web登录用户名、密码的安全性问题。
六、采用RSA非对称性加密算法加密用户名、密码
话不多说,直接上代码
6.1 后端Java相关代码
/**
* @author Alan Chen
* @description
* @date 2019-11-23
*/
public class RsaKey {
//公钥
private String publicKey;
//私钥
private String privateKey;
}
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* @author Alan Chen
* @description 公钥加密-私钥解密 ; 私钥加密-公钥解密
* @date 2019-11-23
*/
public class RsaUtil {
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA";
/**
* 创建RSA 公钥-私钥
* @return
*/
public static RsaKey createKeys(){
return createKeys(1024);
}
/**
* 创建RSA 公钥-私钥
* @param keySize
* @return
*/
public static RsaKey createKeys(int keySize){
//为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg = null;
try{
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
}catch(NoSuchAlgorithmException e){
e.printStackTrace();
}
//初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
//生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
//得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = new String(Base64.encodeBase64(publicKey.getEncoded()));
//得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = new String(Base64.encodeBase64(privateKey.getEncoded()));
RsaKey rsaKey = new RsaKey();
rsaKey.setPublicKey(publicKeyStr);
rsaKey.setPrivateKey(privateKeyStr);
return rsaKey;
}
/**
* 公钥加密
* @param originalText 原文
* @param publicKey
* @return
*/
public static String publicEncrypt(String originalText, String publicKey){
RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
return publicEncrypt(originalText,rsaPublicKey);
}
/**
* 公钥解密
* @param cipherText
* @param publicKey
* @return
*/
public static String publicDecrypt(String cipherText, String publicKey){
RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
return publicDecrypt(cipherText,rsaPublicKey);
}
/**
* 私钥加密
* @param originalText
* @param privateKey
* @return
*/
public static String privateEncrypt(String originalText, String privateKey){
RSAPrivateKey rsaPrivateKey= getPrivateKey(privateKey);
return privateEncrypt(originalText,rsaPrivateKey);
}
/**
* 私钥解密
* @param cipherText 密文
* @param privateKey
* @return
*/
public static String privateDecrypt(String cipherText, String privateKey){
RSAPrivateKey rsaPrivateKey= getPrivateKey(privateKey);
return privateDecrypt(cipherText,rsaPrivateKey);
}
/**
* 得到公钥
* @param publicKey 密钥字符串(经过base64编码)
* @throws Exception
*/
private static RSAPublicKey getPublicKey(String publicKey) {
//通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = null;
try {
keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = null;
try {
key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
return key;
}
/**
* 公钥加密
* @param originalText
* @param publicKey
* @return
*/
private static String publicEncrypt(String originalText, RSAPublicKey publicKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, originalText.getBytes(CHARSET), publicKey.getModulus().bitLength()));
}catch(Exception e){
throw new RuntimeException("加密字符串[" + originalText + "]时遇到异常", e);
}
}
/**
* 得到私钥
* @param privateKey 密钥字符串(经过base64编码)
* @throws Exception
*/
private static RSAPrivateKey getPrivateKey(String privateKey){
//通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = null;
try {
keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = null;
try {
key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
return key;
}
/**
* 私钥解密
* @param cipherText
* @param privateKey
* @return
*/
private static String privateDecrypt(String cipherText, RSAPrivateKey privateKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(cipherText), privateKey.getModulus().bitLength()), CHARSET);
}catch(Exception e){
throw new RuntimeException("解密字符串[" + cipherText + "]时遇到异常", e);
}
}
private static String privateEncrypt(String originalText, RSAPrivateKey privateKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, originalText.getBytes(CHARSET), privateKey.getModulus().bitLength()));
}catch(Exception e){
throw new RuntimeException("加密字符串[" + originalText + "]时遇到异常", e);
}
}
private static String publicDecrypt(String cipherText, RSAPublicKey publicKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(cipherText), publicKey.getModulus().bitLength()), CHARSET);
}catch(Exception e){
throw new RuntimeException("解密字符串[" + cipherText + "]时遇到异常", e);
}
}
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize){
int maxBlock = 0;
if(opmode == Cipher.DECRYPT_MODE){
maxBlock = keySize / 8;
}else{
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try{
while(datas.length > offSet){
if(datas.length-offSet > maxBlock){
buff = cipher.doFinal(datas, offSet, maxBlock);
}else{
buff = cipher.doFinal(datas, offSet, datas.length-offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
}catch(Exception e){
throw new RuntimeException("加解密阀值为["+maxBlock+"]的数据时发生异常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
}
/**
* @author Alan Chen
* @description
* @date 2019-11-23
*/
public class CreateRsaKey {
public static void main (String[] args){
RsaKey rsaKey = RsaUtil.createKeys();
System.out.println("公钥" );
System.out.println(rsaKey.getPublicKey());
System.out.println("私钥");
System.out.println(rsaKey.getPrivateKey());
String str = "alanchen";
System.out.println("明文:" + str);
System.out.println("公钥加密——私钥解密");
//公钥加密
String encodedData = RsaUtil.publicEncrypt(str,rsaKey.getPublicKey());
System.out.println("公钥加密后密文:" + encodedData);
//私钥解密
String decodedData = RsaUtil.privateDecrypt(encodedData, rsaKey.getPrivateKey());
System.out.println("私钥解密后文字: " + decodedData);
}
}
在你之前的授权代码中将前端传过来的密文用你的私钥进行解密,代码大概是这样
username = RsaUtil.privateDecrypt(username,"你的私钥");
password = RsaUtil.privateDecrypt(password,"你的私钥");
6.2 前端React相关代码
导入jsencrypt库
import JSEncrypt from 'jsencrypt';
配置公钥
//后端生成RSA公钥-私钥对后给出公钥
const loginRsaPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYEUuQvo6GHyAid3lVlAG/CIoj9OwljFaNHP5OjBwXxHvucRxVVhgrO2UarRGmLWGH145IkWgMVRnT4/n4UwfbnxHPdrGn0kc7OlpuCQlUlccRByINSB9jcX5QYemswrqHxr+jyMiqrjtioftfjksy2uNhYNBMi82hjgo6o7G/swIDAQAB"
对用户名、密码用公钥进行加密
/**
* Spring Security授权
* @param {*} params
*/
export async function authority(params) {
const { username, password } = params;
var jsencrypt = new JSEncrypt();
jsencrypt.setPublicKey(loginRsaPublicKey);
var cipherPassword = jsencrypt.encrypt(password);
cipherPassword = cipherPassword.replace(/\+/g,'%2B'); //对+号进行转义字符,不然传输到后端会变成空格
var cipherUsername = jsencrypt.encrypt(username);
cipherUsername = cipherUsername.replace(/\+/g,'%2B'); //对+号进行转义字符,不然传输到后端会变成空格
return request(`${baseServerURL}/login?username=${cipherUsername}&&password=${cipherPassword}`, {
method: 'POST'
});
}
前端代码大概是这样
参考资料/推荐阅读
Web登录其实没那么简单
react中使用一些加密库进行RSA、md5、base64加密
使用jsencrypt(rsa加密方式)给js加密防被刷