安全測試須要, 爲防止後臺響應數據返給前臺過程當中被篡改前臺再拿被篡改後的數據進行接下來的操做影響正常業務, 決定採用RSA對響應數據進行簽名和驗籤, 因而有了這篇<RSA後臺簽名前臺驗籤的應用>.html
我這裏所謂的返給前臺的數據只是想加密用戶驗證經過與否的字段success是true仍是false, 前臺拿這個success做爲判斷依據進行下一步的操做, 是進一步向後臺發起請求仍是直接彈出錯誤消息.照測試結果看這是個邏輯漏洞, 即便後臺返回的是false, 在返回前臺的過程當中響應包被劫獲, 將false改成true, 這樣的操做也是能作到的(BurpSuit). 因此後臺響應數據儘可能不要再二次使用. 那既然能篡改, 如何防止流氓篡改呢?java
說一下總體思路: 首先生成密鑰對, 私鑰存放在後臺用於簽名, 公鑰存放在前臺用於驗籤. 所謂簽名就是指拿明文+私鑰生成的簽名結果, 返回數據給前臺時將明文+簽名結果一併返給前臺, 前臺用公鑰+接收到的明文+簽名結果進行驗籤, 這樣即便響應包被劫獲, 篡改明文後, 驗證簽名時也不會驗證經過, 從而達到響應數據防篡改的目的.git
接下來講一下具體步驟.github
採用在線工具生成密鑰對, 私鑰密碼可填可不填, 網址:http://web.chacuo.net/netrsakeypair 截圖中的密鑰對是寫博客時從新生成的, 和代碼中的不同不要見怪~web
生成密鑰對備用.ajax
Controller層java代碼算法
private AjaxJson getAjaxJson(HttpServletRequest req, HttpServletResponse res) { AjaxJson j = new AjaxJson(); 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 = new BigInteger(sessionCounterStr); int clientCount = clientManager.getAllClient().size(); BigInteger clients = new BigInteger(String.valueOf(clientCount)); if (clients.compareTo(counterParam) >= 0) { j.setSuccess(false); j.setRsaStr(GetRSAStr.getResStr(false));// notpass明文 j.setMsg("系統已達最大會話數!"); return j; }
......
簡單說一下這段代碼邏輯, 這是校驗登陸用戶用戶名密碼的其中一段代碼.(關鍵代碼已加粗)apache
思路就是進入該方法時, 把將要返回給前臺的結果ajaxJson中首先設置兩個參數, 一個屬性名爲rsaStr, 另外一個屬性名爲sign.後端
其中rsaStr用於存放隨機生成的uuid, sign用於存放由該uuid和第1步的私鑰生成的簽名結果.安全
當程序中遇到用戶校驗不經過時生成另外一個uuid替換上面的rsaStr的值(sign並不替換), 由ajaxJson一併返回給前臺.
簽名用到的工具類RSAEnDeUtils.java和生成uuid的工具類GetRSAStr.java以下:
RSAEnDeUtils.java
package org.jeecgframework.web.system.util; 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; import java.util.UUID; public class RSAEnDeUtils { // 私鑰 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 static String getPrivateKey() { return PRIVATE_KEY; } /** * RSA最大加密明文大小 */ private static final int MAX_ENCRYPT_BLOCK = 117; /** * RSA最大解密密文大小 */ private static final int MAX_DECRYPT_BLOCK = 128; /** * 獲取密鑰對 * * @return 密鑰對 */ public static KeyPair getKeyPair() throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(1024); return generator.generateKeyPair(); } /** * 獲取私鑰 * * @param privateKey 私鑰字符串 * @return */ public static PrivateKey getPrivateKey(String privateKey) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); byte[] decodedKey = Base64.decodeBase64(privateKey.getBytes()); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); return keyFactory.generatePrivate(keySpec); } /** * 獲取公鑰 * * @param publicKey 公鑰字符串 * @return */ public static PublicKey getPublicKey(String publicKey) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); byte[] decodedKey = Base64.decodeBase64(publicKey.getBytes()); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey); return keyFactory.generatePublic(keySpec); } /** * RSA加密 * * @param data 待加密數據 * @param publicKey 公鑰 * @return */ public static String encrypt(String data, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); 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(Base64.encodeBase64String(encryptedData)); } /** * RSA解密 * * @param data 待解密數據 * @param privateKey 私鑰 * @return */ public static String decrypt(String data, PrivateKey privateKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); 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 簽名 */ public static String sign(String data, PrivateKey privateKey) throws Exception { byte[] keyBytes = privateKey.getEncoded(); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey key = keyFactory.generatePrivate(keySpec); Signature signature = Signature.getInstance("MD5withRSA"); signature.initSign(key); signature.update(data.getBytes()); return new String(Base64.encodeBase64(signature.sign())); } /** * 驗籤 * * @param srcData 原始字符串 * @param publicKey 公鑰 * @param sign 簽名 * @return 是否驗籤經過 */ 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("RSA"); PublicKey key = keyFactory.generatePublic(keySpec); Signature signature = Signature.getInstance("MD5withRSA"); signature.initVerify(key); signature.update(srcData.getBytes()); return signature.verify(Base64.decodeBase64(sign.getBytes())); } public static void main(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以前想到的方案, 不可行)
package org.jeecgframework.web.system.util; import java.io.UnsupportedEncodingException; import java.util.UUID; public class GetRSAStr { public static String getResStr(boolean b) { 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(); }*/ return resStr; } }
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文件以下, rsaverify.js和jsrsasign-all-min.js這兩個文件, 在jsp中引入便可.
<%--邏輯漏洞修復,需引入下面js文件--%> <script src="<%=basePath%>/plug-in/ace/js/rsaverify.js"></script> <script src="<%=basePath%>/plug-in/ace/js/jsrsasign-all-min.js"></script>
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); // 傳入待籤明文 return signature.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代碼格式化了一下(算了, 格式化後一萬兩千多行仍是自行下載吧).
其餘的也沒什麼可說的了, 網上關於前臺加密後臺解密的不少, 後臺簽名前臺驗籤的寥寥無幾, 查看的資料不少不一一列舉了, 在這裏感謝一下https://www.jianshu.com/p/32ab410c71c3
關於後臺加密前臺解密的, 若是有須要能夠參考個人另外一片文章:RSA前臺加密後臺解密的應用
關於登陸過程當中涉及到的傳輸保密性及完整性以及存儲保密性和完整性描述:
先後臺用到的關鍵文件可參考我另外一篇博客, 這裏只做描述, 不做展開, 詳細參考:關於密碼傳輸和密碼存儲的保密性和完整性.