写在前面
安全测试需要, 为防止后台响应数据返给前台过程中被篡改前台再拿被篡改后的数据进行接下来的操作影响正常业务, 决定采用RSA对响应数据进行签名和验签, 于是有了这篇.
我这里所谓的返给前台的数据只是想加密用户验证通过与否的字段success是true还是false, 前台拿这个success作为判断依据进行下一步的操作, 是进一步向后台发起请求还是直接弹出错误消息.照测试结果看这是个逻辑漏洞, 即使后台返回的是false, 在返回前台的过程中响应包被劫获, 将false改为true, 这样的操作也是能做到的(BurpSuit). 所以后台响应数据尽量不要再二次使用. 那既然能篡改, 如何防止流氓篡改呢?
说一下整体思路: 首先生成密钥对, 私钥存放在后台用于签名, 公钥存放在前台用于验签. 所谓签名就是指拿明文+私钥生成的签名结果, 返回数据给前台时将明文+签名结果一并返给前台, 前台用公钥+接收到的明文+签名结果进行验签, 这样即使响应包被劫获, 篡改明文后, 验证签名时也不会验证通过, 从而达到响应数据防篡改的目的.
接下来说一下具体步骤.
正文(具体步骤)
1.在线生成密钥对
采用在线工具生成密钥对, 私钥密码可填可不填, 网址:http://web.chacuo.net/netrsakeypair 截图中的密钥对是写博客时重新生成的, 和代码中的不一样不要见怪~
生成密钥对备用.
2.后台签名
Controller层java代码
privateAjaxJson getAjaxJson(HttpServletRequest req, HttpServletResponse res) {
AjaxJson j= newAjaxJson();
String passresStr= GetRSAStr.getResStr(true);//pass明文
j.setRsaStr(passresStr);//明文
try{
j.setSign(RSAEnDeUtils.sign(passresStr, RSAEnDeUtils.getPrivateKey(RSAEnDeUtils.getPrivateKey())));//pass签名
} catch(Exception e) {
e.printStackTrace();
}//============================client判断开始============================
String sessionCounterStr =sysConfigService.queryConfValueByConfId(SysParamConfig.forSecurityTest.SESSION_COUNTER.getParam());
BigInteger counterParam= newBigInteger(sessionCounterStr);int clientCount =clientManager.getAllClient().size();
BigInteger clients= newBigInteger(String.valueOf(clientCount));if (clients.compareTo(counterParam) >= 0) {
j.setSuccess(false);
j.setRsaStr(GetRSAStr.getResStr(false));//notpass明文
j.setMsg("系统已达最大会话数!");returnj;
}
......
简单说一下这段代码逻辑, 这是校验登录用户用户名密码的其中一段代码.(关键代码已加粗)
思路就是进入该方法时, 把将要返回给前台的结果ajaxJson中首先设置两个参数, 一个属性名为rsaStr, 另一个属性名为sign.
其中rsaStr用于存放随机生成的uuid, sign用于存放由该uuid和第1步的私钥生成的签名结果.
当程序中遇到用户校验不通过时生成另一个uuid替换上面的rsaStr的值(sign并不替换), 由ajaxJson一并返回给前台.
后台关键代码(两个工具类)
签名用到的工具类RSAEnDeUtils.java和生成uuid的工具类GetRSAStr.java如下:
RSAEnDeUtils.java
packageorg.jeecgframework.web.system.util;importorg.apache.commons.codec.binary.Base64;importjavax.crypto.Cipher;importjava.io.ByteArrayOutputStream;import java.security.*;importjava.security.spec.PKCS8EncodedKeySpec;importjava.security.spec.X509EncodedKeySpec;importjava.util.UUID;public classRSAEnDeUtils {//私钥
private static final String PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMlEZXt7J32l4s84ioWDeiKidaqmauNWKTbDInNaq/yK3fIC+j+jg5HjTJutk8ernbqTqeC+oc4I0m+Gs3vBc1QQhP49fIu7B9Y/TgjgQMFLcGfctxMwCcZWgiRrR/k7qWjcjRi09bCfKFxCGsda5OJ60YLQI3C54jXUm6rw1XafAgMBAAECgYAii9PjcwsfPQcGTI0yR5QCN+J8jR4RsWtXk/zo0eptaaSY8rvjinx94Qb4Pb386s8jBE+HXRFG3SrJq9RI7LaPrGjU3qbURTExr9qRo9//eR9VahCKyftryRkeXGqBcOreDgbiTb6wYzUL9OdgSV4to4hz7oIBmnal3+oy5grpIQJBAOgQwoMgAQfjfDSeBcXRklLestWvHRxLu3mpgcvcqHWmeH6HdSxBidJlu0U14QkruvxOZAeW0Y4iu20LY0JKZY8CQQDeBneXmEJr1Pnd/GAUo61i9xpKJOmGmFaiM78DE+JYFdnim+wdye1z/u7GPuD6HmcQC3kb7zpSRVSdOWsnxvXxAkEAhJBWXMsia5wybmg6ifcebAJVDCW9LlXAoU4IHClPfe17dWPxtjc2AJ8ma/HMPA3kAY7SK1enG1eR00enCs4u1wJBAJitY9H4Xzyd0VGIul2XDKVwfUCdT4VB/tk9sk2gf9bI9/Mv+9ekQ0iv92yWUslM3NyYtyixgq6OhJg1ou1QkVECQB3Vu4KvKafP5ejMPe3XplyDI20HJbHlAWH5NGZ67oRWLsVnKAIyLxZRhF4LPXew3gC9BVFCw8zj1geO42oOAso=";public staticString getPrivateKey() {returnPRIVATE_KEY;
}/*** RSA最大加密明文大小*/
private static final int MAX_ENCRYPT_BLOCK = 117;/*** RSA最大解密密文大小*/
private static final int MAX_DECRYPT_BLOCK = 128;/*** 获取密钥对
*
*@return密钥对*/
public static KeyPair getKeyPair() throwsException {
KeyPairGenerator generator= KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);returngenerator.generateKeyPair();
}/*** 获取私钥
*
*@paramprivateKey 私钥字符串
*@return
*/
public static PrivateKey getPrivateKey(String privateKey) throwsException {
KeyFactory keyFactory= KeyFactory.getInstance("RSA");byte[] decodedKey =Base64.decodeBase64(privateKey.getBytes());
PKCS8EncodedKeySpec keySpec= newPKCS8EncodedKeySpec(decodedKey);returnkeyFactory.generatePrivate(keySpec);
}/*** 获取公钥
*
*@parampublicKey 公钥字符串
*@return
*/
public static PublicKey getPublicKey(String publicKey) throwsException {
KeyFactory keyFactory= KeyFactory.getInstance("RSA");byte[] decodedKey =Base64.decodeBase64(publicKey.getBytes());
X509EncodedKeySpec keySpec= newX509EncodedKeySpec(decodedKey);returnkeyFactory.generatePublic(keySpec);
}/*** RSA加密
*
*@paramdata 待加密数据
*@parampublicKey 公钥
*@return
*/
public static String encrypt(String data, PublicKey publicKey) throwsException {
Cipher cipher= Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);int inputLen =data.getBytes().length;
ByteArrayOutputStream out= newByteArrayOutputStream();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 newString(Base64.encodeBase64String(encryptedData));
}/*** RSA解密
*
*@paramdata 待解密数据
*@paramprivateKey 私钥
*@return
*/
public static String decrypt(String data, PrivateKey privateKey) throwsException {
Cipher cipher= Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);byte[] dataBytes =Base64.decodeBase64(data);int inputLen =dataBytes.length;
ByteArrayOutputStream out= newByteArrayOutputStream();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");
}/*** 签名
*
*@paramdata 待签名数据
*@paramprivateKey 私钥
*@return签名*/
public static String sign(String data, PrivateKey privateKey) throwsException {byte[] keyBytes =privateKey.getEncoded();
PKCS8EncodedKeySpec keySpec= newPKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory= KeyFactory.getInstance("RSA");
PrivateKey key=keyFactory.generatePrivate(keySpec);
Signature signature= Signature.getInstance("MD5withRSA");
signature.initSign(key);
signature.update(data.getBytes());return newString(Base64.encodeBase64(signature.sign()));
}/*** 验签
*
*@paramsrcData 原始字符串
*@parampublicKey 公钥
*@paramsign 签名
*@return是否验签通过*/
public static boolean verify(String srcData, PublicKey publicKey, String sign) throwsException {byte[] keyBytes =publicKey.getEncoded();
X509EncodedKeySpec keySpec= newX509EncodedKeySpec(keyBytes);
KeyFactory keyFactory= KeyFactory.getInstance("RSA");
PublicKey key=keyFactory.generatePublic(keySpec);
Signature signature= Signature.getInstance("MD5withRSA");
signature.initVerify(key);
signature.update(srcData.getBytes());returnsignature.verify(Base64.decodeBase64(sign.getBytes()));
}public static voidmain(String[] args) {try{//生成密钥对//KeyPair keyPair = getKeyPair();//String privateKey = new String(Base64.encodeBase64(keyPair.getPrivate().getEncoded()));//String publicKey = new String(Base64.encodeBase64(keyPair.getPublic().getEncoded()));
String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMlEZXt7J32l4s84ioWDeiKidaqmauNWKTbDInNaq/yK3fIC+j+jg5HjTJutk8ernbqTqeC+oc4I0m+Gs3vBc1QQhP49fIu7B9Y/TgjgQMFLcGfctxMwCcZWgiRrR/k7qWjcjRi09bCfKFxCGsda5OJ60YLQI3C54jXUm6rw1XafAgMBAAECgYAii9PjcwsfPQcGTI0yR5QCN+J8jR4RsWtXk/zo0eptaaSY8rvjinx94Qb4Pb386s8jBE+HXRFG3SrJq9RI7LaPrGjU3qbURTExr9qRo9//eR9VahCKyftryRkeXGqBcOreDgbiTb6wYzUL9OdgSV4to4hz7oIBmnal3+oy5grpIQJBAOgQwoMgAQfjfDSeBcXRklLestWvHRxLu3mpgcvcqHWmeH6HdSxBidJlu0U14QkruvxOZAeW0Y4iu20LY0JKZY8CQQDeBneXmEJr1Pnd/GAUo61i9xpKJOmGmFaiM78DE+JYFdnim+wdye1z/u7GPuD6HmcQC3kb7zpSRVSdOWsnxvXxAkEAhJBWXMsia5wybmg6ifcebAJVDCW9LlXAoU4IHClPfe17dWPxtjc2AJ8ma/HMPA3kAY7SK1enG1eR00enCs4u1wJBAJitY9H4Xzyd0VGIul2XDKVwfUCdT4VB/tk9sk2gf9bI9/Mv+9ekQ0iv92yWUslM3NyYtyixgq6OhJg1ou1QkVECQB3Vu4KvKafP5ejMPe3XplyDI20HJbHlAWH5NGZ67oRWLsVnKAIyLxZRhF4LPXew3gC9BVFCw8zj1geO42oOAso=";
String publicKey= "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJRGV7eyd9peLPOIqFg3oionWqpmrjVik2wyJzWqv8it3yAvo/o4OR40ybrZPHq526k6ngvqHOCNJvhrN7wXNUEIT+PXyLuwfWP04I4EDBS3Bn3LcTMAnGVoIka0f5O6lo3I0YtPWwnyhcQhrHWuTietGC0CNwueI11Juq8NV2nwIDAQAB";
System.out.println("私钥:" +privateKey);
System.out.println("公钥:" +publicKey);//RSA加密//String data = "123456";
UUID uuid =UUID.randomUUID();
String data= uuid.toString();//.toString().replace("-","")
String encryptData =encrypt(data, getPublicKey(publicKey));
System.out.println("加密后内容:" +encryptData);//RSA解密
String decryptData =decrypt(encryptData, getPrivateKey(privateKey));
System.out.println("解密后内容:" +decryptData);//RSA签名
String sign =sign(data, getPrivateKey(privateKey));
System.out.println("签名:" +sign);//RSA验签
boolean result =verify(data, getPublicKey(publicKey), sign);
System.out.print("验签结果:" +result);
}catch(Exception e) {
e.printStackTrace();
System.out.print("加解密异常");
}
}
}
GetRSAStr.java(忽略备注释的代码吧, 那是RSA之前想到的方案, 不可行)
packageorg.jeecgframework.web.system.util;importjava.io.UnsupportedEncodingException;importjava.util.UUID;public classGetRSAStr {public static String getResStr(booleanb) {
String resStr= "";if(b) {
UUID uuid=UUID.randomUUID();
String passuuidStr= uuid.toString().replace("-", "");
resStr=passuuidStr;
}else{
UUID uuid=UUID.randomUUID();
String notpassuuidStr= uuid.toString().replace("-", "");
resStr=notpassuuidStr;
}/*java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
final byte[] textByte;
try {
textByte = resStr.getBytes("UTF-8");
String encodedText = encoder.encodeToString(textByte);
resStr = new StringBuilder(encodedText).reverse().toString();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}*/
returnresStr;
}
}
3.前台验签
jsp中js部分
$.ajax({
async:false,
cache:false,
type:'POST',
url: checkurl,//请求的action路径
data: formData,
error:function () {//请求失败处理函数
},
success:function(data) {var d =$.parseJSON(data);var success =d.success;//验证签名start===========
var rsaStr = d.rsaStr;//明文
var sign = d.sign;//签名
const rsaverify = RSA_VERIFY_SIGN(publicKey, rsaStr, sign);//验签结果
//验证签名end===========
if(rsaverify) {
window.location.href=actionurl;
}else{
showErrorMsg(d.msg);
......
这段就是登陆方法的其中一段代码, 忽略这坨翔吧, 看一下思路, 公钥也即是publicKey是定义在js文件中的全局变量, rsaStr和sign都是由后台响应数据data中获取的, 在采用rsa之前if else判断条件是用的var success = d.success;中success的结果, 由于该结果有可能被篡改, 才采用了现在的验签结果rsaverify作为判断依据.
前台关键代码(js文件)
验签用到的js文件如下, rsaverify.js和jsrsasign-all-min.js这两个文件, 在jsp中引入即可.
rsaverify.js
const ALGORITHM = 'MD5withRSA';/**
* 私钥签名
* rsa 用 MD5withRSA 算法签名
* @param privateKey 私钥
* @param src 明文
* @return {*}
* @constructor*/const RSA_SIGN= (privateKey, src) =>{
const signature= new KJUR.crypto.Signature({'alg': ALGORITHM});
const priKey= KEYUTIL.getKey(privateKey); //因为后端提供的是pck#8的密钥对,所以这里使用 KEYUTIL.getKey来解析密钥
signature.init(priKey); //初始化实例
signature.updateString(src); //传入待签明文
const a = signature.sign(); //签名, 得到16进制字符结果
return hex2b64(a) //转换成base64
};/**
* 公钥验签
* @param publicKey 公钥
* @param src 明文
* @param data 经过私钥签名并且转换成base64的结果
* @return {Boolean} 是否验签成功
* @constructor*/const RSA_VERIFY_SIGN= (publicKey, src, data) =>{
const signature= new KJUR.crypto.Signature({'alg': ALGORITHM, 'prvkeypem': publicKey});
signature.updateString(src);//传入待签明文
returnsignature.verify(b64tohex(data))
};
const publicKey= '-----BEGIN PUBLIC KEY-----\n' +
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJRGV7eyd9peLPOIqFg3oionWq\n' +
'pmrjVik2wyJzWqv8it3yAvo/o4OR40ybrZPHq526k6ngvqHOCNJvhrN7wXNUEIT+\n' +
'PXyLuwfWP04I4EDBS3Bn3LcTMAnGVoIka0f5O6lo3I0YtPWwnyhcQhrHWuTietGC\n' +
'0CNwueI11Juq8NV2nwIDAQAB\n' +
'-----END PUBLIC KEY-----';
jsrsasign-all-min.js库可以从GitHub上下载, 地址:https://github.com/kjur/jsrsasign, 也可以直接拷贝下面代码.只不过我为了便于观看代码把min代码格式化了一下(算了, 格式化后一万两千多行还是自行下载吧).
其他的也没什么可说的了, 网上关于前台加密后台解密的很多, 后台签名前台验签的寥寥无几, 查看的资料很多不一一列举了, 在这里感谢一下
关于后台加密前台解密的, 如果有需要可以参考我的另一片文章:
传输以及存储的保密性和完整性(补充)
关于登录过程中涉及到的传输保密性及完整性以及存储保密性和完整性描述:
前后台用到的关键文件可参考我另一篇博客, 这里只作描述, 不作展开, 详细参考:
感谢