对网络安全的要求最近是越来越高,尤其是互联网上提供服务的系统,要求敏感信息(如登录用户名、密码、用户真名、手机号码)都要加密传输。
加密传输的含义是:在前端用户输入的页面上,要先用前端代码(如 JS)加密, SUBMIT 到后台来,在后台对加密串进行解密。这样的话,网络上的任何对传输流的拦截都不再产生风险,因为看到的都是一串串密文。
关键词:前端 JS 加密,后端 JAVA 代码解密。
就是说:两种技术路线的加解密算法,必须是一致的,否则不可能解密还原。听着就很难,好在大神们已经帮我们做好了,那就是 RSA 加密工具包。
到CSDN的下载资源区去找,很多:前端 JS 包 security.js ;后端是采用 org.bouncycastle.jce.provider.BouncyCastleProvider 提供的加密解密类,在包 bcprov-jdk14-145.jar 中 。
RSA 是著名的非对称加密算法,十分玄妙,利用的是数学中的“互质”概念,来达成一种“锁”和“钥匙”的效果。
前端加密和后端解密整个过程,用容易理解的通俗语言可以描述为:
1. 客户端在填写登录表单时,同时跟后端要了一把锁(公钥),并用 JS 语言使用这把锁(公钥)为自己的用户名和密码加密了;
2. 后端在响应前端的请求生成这把锁(公钥)的时候,同步生成了能开这把锁的一枚钥匙(公钥对应的私钥),自己存起来了;
3. 客户端将用户名、密码、公钥一起 SUBMIT 给后端;
4. 后端根据提交上来的公钥,找到对应的保存起来的私钥,对用户名密码解密,得到原始的客户端填写的值。
这个过程我们可以看到,解密用的私钥并没有在网络上传递,而光有传递的公钥是无法对加密串做解密的,这样就保证了安全。
非对称加密就是上锁的公钥即使被别人窃取了,但是窃锁人没有私钥,也打不开锁,没用。
明白了原理,我们来上代码:
前端 JS:
<%
String IModulus = "";
String IExponent = "";
try {
Map keys = RSAUtil.getKeyMap(1);
RSAPublicKey pubKey = (RSAPublicKey)keys.get("publicKey");
IModulus = pubKey.getModulus().toString(16);
IExponent = pubKey.getPublicExponent().toString(16);
}
catch (Exception e) {
//e.printStackTrace();
}
%>
如上这段就是表单页面跟后端要锁的过程,得到的公钥表现为两个值:IModulus、IExponent,将这两个值赋值给 HTML 页面的元素,以备 JS 加密时使用。
下面这段 JS 函数,就是在 SUBMIT 前对页面上输入的字符串做加密
function RSAEncrypeValue (inValue) {
var Modulus = document.getElementById("Modulus").value; //加密模
var public_exponent = document.getElementById("Exponent").value; //公钥指数
var myPubKey = new RSAUtils.getKeyPair(public_exponent, "", Modulus); //通过模和公钥参数获取公钥
var UnicodeValue = encodeURIComponent(inValue); //转义为 UNICODE ,以兼容中英文和各种符号
var ReverseValue = UnicodeValue.split("").reverse().join(""); //颠倒被加密串的顺序,要不然后解密后会发现密码顺序是反的
var EncrypedValue = RSAUtils.encryptedString(myPubKey, ReverseValue); //对密码进行加密传输(这个加密函数,是有长度限制的,超过 20 个字符的串会被分段加密,每段之间用空格分隔,后端在解密时要做分段分别处理,并合理拼接起来)
return EncrypedValue;
}
后端 JAVA:先实现一个秘钥工具类 RSAUtil(我用一个静态hashMap 来缓存一个秘钥对,每次应用起来后生成一对秘钥,后续就一直使用这对秘钥即可,无需每个请求都新生成秘钥对,那样会造成性能消耗)
public class RSAUtil {
static HashMap<String,Object> cacheMap = null;
static public HashMap<String,Object> getKeyMap(int size) {
if (cacheMap == null) {
cacheMap = createKey(size);
}
HashMap<String,Object> retMap = (HashMap<String,Object>) cacheMap.clone();
return retMap;
}
/**
* 自动生成密钥对
* @throws Exception
*/
static public HashMap<String,Object> createKey( int size){
int nSize = (size < 256) ? 256 : size;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
SecureRandom random = new SecureRandom();
keyPairGenerator.initialize(nSize, random);
// 生成钥匙对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 得到公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 得到私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
HashMap<String,Object> map = new HashMap<String, Object>();
map.put("publicKey", publicKey);
map.put("privateKey", privateKey);
/* 把私钥保存到硬盘上
saveKey(privateKey,"C:/hyhtemp/private_key");
//把公钥保存到硬盘上
saveKey(publicKey,"C:/hyhtemp/public_key"); */
return map;
}
catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
/**
* 加密(用公钥)
* @param publicKey 公钥
* @param content 需要加密的内容
* @return
* @throws Exception
*/
static public String encrypttoStr(Key publicKey,String content) throws Exception{
String endata = parseByte2HexStr( publicEnrypy (publicKey, content) );
return endata;
}
/**
* 解密(用私钥)
* @param privateKey 私钥
* @param endata 需要解密的内容
* @return
* @throws Exception
*/
static public String decrypttoStr(Key privateKey,String endata) throws Exception{
String data = new String(privateEncode(privateKey, parseHexStr2Byte(endata)));
return data;
}
static public String decrypttoStr_normal(Key privateKey,String endata) throws Exception{
String data = new String(privateEncode(privateKey, endata.getBytes()));
return data;
}
//
/**
* 加密的方法,使用公钥进行加密
* @param publicKey 公钥
* @param data 需要加密的数据
* @throws Exception
*/
private static byte[] publicEnrypy(Key publicKey,String data) throws Exception {
/* 这句的含义是:我们的 RSA 算法是用 org.bouncycastle.jce.provider.BouncyCastleProvider 这个类具体实现的,这个类在 JAR 包 bcprov-jdk14-145.jar 中 */
Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider() );
// 设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 对数据进行加密
byte[] result = cipher.doFinal(data.getBytes());
return result;
}
/**
* 解密的方法,使用私钥进行解密
* privateKey 私钥
* encoData 需要解密的数据
* @throws Exception
*/
private static byte[] privateEncode(Key privateKey,byte[] encoData) throws Exception {
Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
//设置为解密模式,用私钥解密
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//解密
byte[] data = cipher.doFinal(encoData);
// System.out.println("解密后的数据:"+data);
return data;
}
/**将二进制转换成16进制
* @param buf
* @return String
*/
static private String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**将16进制转换为二进制
* @param hexStr
* @return byte[]
*/
static private byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
}
有了上述工具类,我们就可以开始对前端提交上来的加密串做解密工作了(只需要改造从 REQUEST 获取到的 USERNAME PASSWORD 即可):
public String setUserName(String strValue) {
String UserName = "";
try {
Map keys = RSAUtil.getKeyMap(1);
RSAPrivateKey privateKey = (RSAPrivateKey)keys.get("privateKey"); /* 从工具类获取私钥 */
String strCrytedValue = strValue;
if (Tool.getDebug()) System.out.println("解密前 strCrytedValue: " + strCrytedValue);
/* 因为可能超长,尝试分段解密 2021-08-24 by hanyanhua */
String strTemp = null;
String strWhole = "";
String[] strTempArray = strCrytedValue.split(" ");
if ((strTempArray != null) && (strTempArray.length > 0)) {
for (int i = 0; i < strTempArray.length; i++) {
if (Tool.getDebug()) System.out.println("片段 " + i + ": /" + strTempArray[i] + "/");
strTemp = RSAUtil.decrypttoStr(privateKey, strTempArray[i] );
if (Tool.getDebug()) System.out.println("片段解密后 " + i + ": /" + strTemp + "/");
strWhole = strTemp + strWhole; //注意这里的拼接顺序,因为前端加密前是做了一个reverse动作,所以段与段之间的顺序也是 reverse 的
}
}
strTemp = java.net.URLDecoder.decode(strWhole,"UTF-8");
if (Tool.getDebug()) System.out.println("整段 UTF-8 解码后 : /" + strTemp + "/");
UserName = strTemp.toString();
if (Tool.getDebug()) System.out.println("解密后 UserName: " + UserName);
}
catch (Exception e) {
e.printStackTrace();
} /* 用户名密码加密传输结束 */
return UserName;
}
总结:
- 如果被加密的串可能有中文,注意一定要在真正做加密前转化成 UNICODE 字符串,就是类似 %E5%D2 这样的串,如此一来,每个中文字就会产生 6 个字符;
- JS 前端加密算法有长度限制,限定字符是 30 个,超过 30 字符的会自动分段,段与段之间是空格分隔。
- 相应的,解密端要注意分段处理,各段之间的顺序也是倒序的,拼接时注意。(尤其是中文的UNICODE是有严格顺序的,所以一定要注意)
- 如果被加密的是中文,5 个字以上就会超长分隔了。