前言前段時間看到一篇文章講如何保證API調(diào)用時數(shù)據(jù)的安全性(傳送門:https://blog.csdn.net/ityouknow/article/details/80603617),文中講到利用RSA來加密傳輸AES的秘鑰,用AES來加密數(shù)據(jù),并提供如下思路: 說人話就是前、后端各自生成自己的RSA秘鑰對(公鑰、私鑰),然后交換公鑰(后端給前端的是正常的明文公鑰,前端給后端的是用后端公鑰加密后的密文公鑰;PS:其實我覺得直接交換兩個明文公鑰就行了),后端生成AES的明文key,用明文key進(jìn)行AES加密得到密文數(shù)據(jù),用前端的公鑰進(jìn)行RSA加密得到密文key,API交互時并將密文數(shù)據(jù)與密文key進(jìn)行傳輸,前端用自己的私鑰進(jìn)行RAS解密的到明文key,用明文key進(jìn)行AES解密得到明文數(shù)據(jù);前端給后端發(fā)送數(shù)據(jù)時同理,這樣一來,傳輸?shù)臄?shù)據(jù)都是密文,且只有秘鑰才能解密 可惜這篇博客只提供了思路,但并沒有具體的代碼,我們在網(wǎng)上查找一下資料,開始生擼代碼,實現(xiàn)一個前后端API交互數(shù)據(jù)加密——AES與RSA混合加密,并應(yīng)用到項目中 后端加、解密從網(wǎng)上查找工具類,再進(jìn)行改造 先引入Base64工具類 <!-- Base64編碼需要 --> <dependency> <groupId>org.apache.directory.studio</groupId> <artifactId>org.apache.commons.codec</artifactId> <version>1.8</version> </dependency> AES![]() RSA![]() 簡單測試AES對稱加密、解密簡單測試 1、字符串 public static void main(String[] args) { //16位 String key = "MIGfMA0GCSqGSIb3"; //字符串 String str = "huanzi.qch@qq.com:歡子"; try { //加密 String encrypt = AesUtil.encrypt(str, key); //解密 String decrypt = AesUtil.decrypt(encrypt, key); System.out.println("加密前:" + str); System.out.println("加密后:" + encrypt); System.out.println("解密后:" + decrypt); } catch (Exception e) { e.printStackTrace(); } } 加密前:huanzi.qch@qq.com:歡子 加密后:dXPRtcdHPQSTwxLnmixkaSvNfGHhg5Gz8sGTtiqCpPo=解密后:huanzi.qch@qq.com:歡子 2、復(fù)雜對象 String key = "MIGfMA0GCSqGSIb3" ImsUserVo userVo = "123456""111111" String encrypt = String decrypt ="加密前:" +"加密后:" +"解密后:" + 加密前:ImsUserVo(id=null, userName=123456, password=111111, nickName=null, gender=null, avatar=null, email=null, phone=null, sign=null, createdTime=null, updataTime=null) 加密后:AXv8ewfY+gbuZ/dCmGAxngLry+Idlp1NKZ8yyf9+bmrBggUBo3b+e4XRwMAE/DP+vFS2HpgeYQTrZM1ECjo01uvZ/T6lY7b2C6L8PTotYHQyJM3kOs+YNXL/uyvFZ2EICSQWhmM1XX+g0juHLCbgQDMNXc56S/7eH2p+su1+CTMygUBCF0U/gZaSzqylqujTb3sg7q4xMuxCQ6ne6xmL3ebjanOLeMJHypTDy1rlJTw=解密后:ImsUserVo(id=null, userName=123456, password=111111, nickName=null, gender=null, avatar=null, email=null, phone=null, sign=null, createdTime=null, updataTime=null) RAS非對稱加密、解密簡單測試 1、字符串的RSA公鑰加密、私鑰解密 public static void main(String[] args) { //字符串 String str = "huanzi.qch@qq.com:歡子"; try { System.out.println("私鑰:" + RsaUtil.getPrivateKey()); System.out.println("公鑰:" + RsaUtil.getPublicKey()); //公鑰加密 byte[] ciphertext = RsaUtil.encryptByPublicKey(str.getBytes(), RsaUtil.getPublicKey()); //私鑰解密 byte[] plaintext = RsaUtil.decryptByPrivateKey(ciphertext, RsaUtil.getPrivateKey()); System.out.println("公鑰加密前:" + str); System.out.println("公鑰加密后:" + Base64.encodeBase64String(ciphertext)); System.out.println("私鑰解密后:" + new String(plaintext)); } catch (Exception e) { e.printStackTrace(); } } 私鑰:MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANG08b2L0Hk1QCJXyTUI6A4CqW+KENCedZyJCYMteZ/vx93KeYZbPShhI3IWJJtj9U+ibiAVRjzmikI9lkKdgnCaOgTmEZis2RWgLzhcOpSqdp/J6d+YtmCD6UDeO3E6QPyfVv9d3qPrqaYUCxi7CmouzVaa/cJqrfYB7qGYt3u5AgMBAAECgYBbovQX3ebFcG2MFExKLpAovyUHJo/eeb/vHTrY5aBGMWNnGbks6uW4pWn1ypNIi8+AcvwobON6bUtxUrQ8e9OpUlDYTAAqDE8JvJoRC3theHpJbkHCdDLeNnz1EizUwxfe3X3IVwEYd29C00WXt0rUW2D/Fsa7ECp08taeV+ukAQJBAOyfO8opGp8t40bbyMRVsIR2zK19rN6Kd/NGvjjW/7BPgzDJZsybcN6e0AuhFaWTyHNSonpDztEQ0VWhF0mHokECQQDi4W9xCmzQf0l8mgXUP2IDY5YtQN9g9vL51qEwpcxhHxCCcid62R0y6T2GnRTmkEpSwPYZ2EZQrKtpGiEk4wt5AkAhwwqd6sWApuSB7MQ1t2BLVkQYERGEY0+AJ7zmkU7EUmQOpv4C/b7aFODsd9yF1pNIWScTuO8eh37G8AhJlo/BAkEAuIHfME3rGlA5whQ8I1T8b4cgjWLRhrit9tI+OiLLqDwsH/mX88b3gPy/pWa/pZW4a74zJeeFn3wc1heC1s2x+QJAXHVf9fZaFwDlD6nD3x0Sgu8Mdp8tsfdz2wIkvjtANc+eojkfxwdZd6PKWgmiPTLKNNqbPaLgtU74WVAnlpSgsw==公鑰:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRtPG9i9B5NUAiV8k1COgOAqlvihDQnnWciQmDLXmf78fdynmGWz0oYSNyFiSbY/VPom4gFUY85opCPZZCnYJwmjoE5hGYrNkVoC84XDqUqnafyenfmLZgg+lA3jtxOkD8n1b/Xd6j66mmFAsYuwpqLs1Wmv3Caq32Ae6hmLd7uQIDAQAB 公鑰加密前:huanzi.qch@qq.com:歡子 公鑰加密后:MQa65DyVZg/L8SBilLX1yUiajtiTBqUFpQ/qlrSRyMGCubylbp9KisowRghPxk9BuI3+ea/4QpidIZKJaZAbQQ+ZKyslSTk3nm6H+0BF9pMA7BUeC33xHSy+3lJrNOr5S+Vup1Oir3Nu8i2vJYQV1pPkB5+zyUVEcNLD3xr/eNQ=私鑰解密后:huanzi.qch@qq.com:歡子 2、復(fù)雜對象的RSA公鑰加密、私鑰解密 public static void main(String[] args) { //復(fù)雜對象 ImsUserVo userVo = new ImsUserVo(); userVo.setUserName("123456"); userVo.setPassword("111111"); try { System.out.println("私鑰:" + RsaUtil.getPrivateKey()); System.out.println("公鑰:" + RsaUtil.getPublicKey()); //公鑰加密 byte[] ciphertext = RsaUtil.encryptByPublicKey(userVo.toString().getBytes(), RsaUtil.getPublicKey()); //私鑰解密 byte[] plaintext = RsaUtil.decryptByPrivateKey(ciphertext, RsaUtil.getPrivateKey()); System.out.println("公鑰加密前:" + userVo.toString()); System.out.println("公鑰加密后:" + Base64.encodeBase64String(ciphertext)); System.out.println("私鑰解密后:" + new String(plaintext)); } catch (Exception e) { e.printStackTrace(); } } 私鑰:MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAL6gSKs2G4iFrhPo0aLELfGzsCAaB5hztvclD9J2hZT2KXfs6S5JwZ0RWRR28rqHm0e2RNW3fzYyOLvSoq93n/TRAkmXBbVia3BCTrSzLPrKFY8JvLyXqbrV0NrxywY+4ZlgR5R+scWaj3LtUR63sSXb5ddmOg9XctrWBGvsKrNJAgMBAAECgYEAoql9OPPDzNxdbcnGUQDcP5pYGRx9DL75Cq2KccoHNNRVEGuNkp0HZLLv84GIoFikzS2gUUnyeFmkhck4X0hRqYpCo9DwRsBgBpqn+4ebjSu4bd3lG5KCAtMaPC5sAbznY1uuuJnUdul3p9PuF7AmFTsoFFB4YvstvkRna5ZPFA0CQQDfpxPYVpZjOsgng7187vEpFa9vlQxmyamvJ2iAeFLRHCqJwlq4VYqJkgr08SE1XCBqSVhXkLyIPAtdeqxU0iFLAkEA2jJfKVSy4I/BHmk/rdpw7InQ3ERBc/a09t2ZiI3bqtnobTIf/sMZEWPeMkY83RrWL9ZQvMNDa843cans3bm1OwJAIdipGi5QaAf3TnOTc5q9iFgtypcl31BZi5ZNLFQJRHgcv+hXzlmzs4oUemkbe3XLugoLgoT24y8jESyFc/iw7QJBAJdR26EENlF6IIoAn8Ln/Oxt30UCqQnNDE8v+2wyRSdFm+Uun/XEQ7xFsDDZeRg1pljinndqS3WWO+k92SEjy0UCQQCr5UsIMBAjpGCYXeXrRWYoYdfI6+R20I+uWoOGzoly+KK4ixqMLFuimEwrmXhYnJMzvVHfbLsoogBv9NOP9ffH 公鑰:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+oEirNhuIha4T6NGixC3xs7AgGgeYc7b3JQ/SdoWU9il37OkuScGdEVkUdvK6h5tHtkTVt382Mji70qKvd5/00QJJlwW1YmtwQk60syz6yhWPCby8l6m61dDa8csGPuGZYEeUfrHFmo9y7VEet7El2+XXZjoPV3La1gRr7CqzSQIDAQAB 公鑰加密前:ImsUserVo(id=null, userName=123456, password=111111, nickName=null, gender=null, avatar=null, email=null, phone=null, sign=null, createdTime=null, updataTime=null) 公鑰加密后:Un+1m/CbpzVkkxYrwNOWyEXqpsawxcdv4p3G+9b+SQRiC/THL8YG+IvqFCHnxizzYGB9LEvLbQxw72JB0Wlo1+/SvX7AJb2h0ddpvVUkPjmtXNo073SV1zMK+9NTCJUMMoHu/TIptxRbVxlBoGMHa+jq8h2y3RUOPtx/9zhBWlQmzZEifv0MjgAhKX5ucExYfXctcAVGHL959+TwKqKQmTENw5o0ElksaA0KIF+4L7RvpWVSqZT1Y4O2gMP9ALjamCx6ziRcmk4b4Q5Goph0nmw6nA387qVi3Vz6rQHrIpL0HT5OSiz1O7+2L3N0Him2IZeAgg3EZCi5xTGl54jGEw==私鑰解密后:ImsUserVo(id=null, userName=123456, password=111111, nickName=null, gender=null, avatar=null, email=null, phone=null, sign=null, createdTime=null, updataTime=null) 如需使用更多加密、填充方式,引入 <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk16</artifactId> <version>1.46</version></dependency> 加解密的時候改成 Cipher cipher = Cipher.getInstance(ALGORITHMS ,new BouncyCastleProvider()); 重要更新2020-05-21更新 重要Bug修復(fù):后端加解密中,不能在代碼里new BouncyCastleProvider(),JceSecurity. getVerificationResult內(nèi)部會進(jìn)行判斷,如果是新值,則每次都會put到map中,導(dǎo)致內(nèi)存緩便被耗盡,程序假死崩潰(參考博客:https://www./A/lk5aQo7451/ ) 應(yīng)該改成,我已經(jīng)在開源項目改了,博客上面之前貼出來的代碼我就不改了,具體代碼大家去開源項目查看看吧 前端加、解密AES我們采用CryptoJS,是一個標(biāo)準(zhǔn)和安全加密算法的JavaScript庫,它的AES加密支持AES-128、AES-192和AES-256。下載或查看詳情介紹請戳官網(wǎng)地址 GitHub地址:https://github.com/brix/crypto-js 官網(wǎng)地址:https://code.google.com/archive/p/crypto-js/ RSA我們采用JSEncrypt,它是一個很好用的RSA加密算法的JavaScript庫,使用PKCS#1進(jìn)行填充,加解密使用方式很簡單,具體的介紹或者下載請移步官網(wǎng) GitHub地址:https://github.com/travist/jsencrypt 官網(wǎng)地址:http:///jsencrypt/ 下載下來后我們在項目頭部head.html引入,并新建兩個小工具類 AES![]() RSA![]() 簡單測試AES對稱加密、解密簡單測試 1、字符串 //字符串let text = "huanzi.qch@qq.com:歡子";//keylet genKey = aesUtil.genKey();//key加密let ciphertext = aesUtil.encrypt(text,genKey);//key解密let plaintext = aesUtil.decrypt(ciphertext,genKey); console.log("key:");console.log(genKey); console.log("加密前:");console.log(text); console.log("key加密后:" + ciphertext); console.log("key解密后:");console.log(plaintext); key:q99IsnEuryk1ZvgX 加密前:huanzi.qch@qq.com:歡子 key加密后:aZn58GtEj9Is0hNWbJoqpRD6RkiBVPCHOvva3Xq2PYo=key解密后:huanzi.qch@qq.com:歡子 2、復(fù)雜對象 //復(fù)雜對象let user = {username: "歡子", password: 123456, remark: "abcd!@#$:"};//keylet genKey = aesUtil.genKey();//key加密let ciphertext = aesUtil.encrypt(user,genKey);//key解密let plaintext = aesUtil.decrypt(ciphertext,genKey); console.log("key:");console.log(genKey); console.log("加密前:");console.log(user); console.log("key加密后:" + ciphertext); console.log("key解密后:");console.log(plaintext); key:e6gzizHIpDfc6hbg 加密前:{username: "歡子", password: 123456, remark: "abcd!@#$:"} key加密后:YdNw5AwteEp8WZs5xMv0YiGcXvX81P9MCLOvroHjfLUyQV/GwJ6obRqi4DT2ucJy8DWrKueOzLGLSQXUVhAgIA== key解密后:{username: "歡子", password: 123456, remark: "abcd!@#$:"} RAS非對稱加密、解密簡單測試 1、字符串的RSA公鑰加密、私鑰解密 //普通字符串let text = "huanzi.qch@qq.com:歡子";//秘鑰對let keyPair = rsaUtil.genKeyPair();//公鑰加密let ciphertext = rsaUtil.encrypt(text,keyPair.publicKey);//私鑰解密let plaintext = rsaUtil.decrypt(ciphertext,keyPair.privateKey); console.log("秘鑰:");console.log(keyPair.privateKey); console.log("公鑰:" + keyPair.publicKey); console.log("加密前:" + text); console.log("公鑰加密后:" + ciphertext); console.log("解密后:" + plaintext); 秘鑰:MIICXQIBAAKBgQDtBKNg9NJ0+mMWq+99geoi32t+xkbJuvQ4Wr7x8I+zGT8xiG+jG+OAuSjvi5yA7IEMMAj8Y8vS7IPPo2mAr/PH0DsNiHMATJm8mNIEDzfP4WOFOdidzqP+6/9iOLMfe4cHtGq+kdX7QPx4uabnIXAREnR4nVl5Mtxf+vEHXGmEPwIDAQABAoGBANH92gJ85jld3YyoqHa6M4bSC5s2cGEqklWbkLEqQSacp7BrAP2yJ85UPkB9oRtYbr0tkciLYnptshq03TR2r7QT5+ovb5KJ2MQExTXk8GZTO/5sSqD0zwA9SESlAmWj8yc49p5Tk0h5UYFgsRATTer1n1llziryXa4QMiIfKrmBAkEA+za3DryZnMyNqre6Kx+FYsFVvIbHRU7tJ5LOiZ43Vl0DXq44zmeNQeh6MzfH6sc0Avu1c61/+KNDVf23yfVt7QJBAPGIr0GokOZ+L0sttiEoQSq/dBdYaSCBfTht+rA/9ie8RgcFJkYj4h/6RPzdYIRWDco5RzI+oPnZmFC4rfPjFVsCQHE2XFMw3c2TRfj86dKLVxKFbL0UxHNQuYIPIDNW8TtjmaQuwf0LH9bnDUNNzTPaaG87vq+OLlEAStVTDWPfzpUCQDVJvbjTstxXfKGufR9FnVMMGFXKOK9mQjU/9m4KPom3vQ9xcGdLJWl+stfDE7c+sR4rkuyf6q4U9sjgZeiH8j8CQQCfaxXfxiDJPztUDm1AKI6uDwz4P4eiYRbbbQ5x+iQSunHbq0Y7U9UUkWcLw0xDhReHEYkFuOeiBj2ViAPJz1r0 公鑰:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDtBKNg9NJ0+mMWq+99geoi32t+xkbJuvQ4Wr7x8I+zGT8xiG+jG+OAuSjvi5yA7IEMMAj8Y8vS7IPPo2mAr/PH0DsNiHMATJm8mNIEDzfP4WOFOdidzqP+6/9iOLMfe4cHtGq+kdX7QPx4uabnIXAREnR4nVl5Mtxf+vEHXGmEPwIDAQAB 加密前:huanzi.qch@qq.com:歡子 公鑰加密后:aUkUMYC7lF8M1xzcx5ZEdc0DQt4FrqvWqEnD30raV++j7rwsfEcyXpPmeF1g2LR86FVG3oxgdTptorkwUDSXB3Tv4av7toGg7Zcf9l1vs5WQX7kCDTitwBVwyBNTZq22xed1J/LAkDujDav6tUJHdMmRKYVe2NeTswvWLOqWWW4= 解密后:huanzi.qch@qq.com:歡子 2、復(fù)雜對象的RSA公鑰加密、私鑰解密 //復(fù)雜對象let user = {username:"歡子",password:123456,remark:"abcd!@#$:"};//秘鑰對let keyPair = rsaUtil.genKeyPair();//公鑰加密let ciphertext = rsaUtil.encrypt(user,keyPair.publicKey);//私鑰解密let plaintext = rsaUtil.decrypt(ciphertext,keyPair.privateKey); console.log("秘鑰:");console.log(keyPair.privateKey); console.log("公鑰:" + keyPair.publicKey); console.log("加密前:" + user); console.log("公鑰加密后:" + ciphertext); console.log("解密后:" + plaintext); 秘鑰:MIICXAIBAAKBgQCsAE5TN8kD7U4mFyxBzN1w23Rkf4K8MQ3B0bCZE5crjYp81eUtWrfUM+zLPmF9e1P/ws2yGHvL6mueU9PxtDJn5rSLsQBSxIkN0QB/nq76S4uh2Nrmmrjomejy5LqXnTVbEoIW2RTFBzyMWy4AjQY6P2pAJ8zCagvcdYcweUIqMQIDAQABAoGAbwrLhkIvjk938nNnaRufoqqrW+5OMrzgis6bWlghckawr6NPj5ZPs7nKF/Sv79jdA/N55I6V7bHrxI2N+S9Ckm2ygv8nNYimSjzspR48SqVRuH/xYmQQ9hi8Iy4dTlCMud34oXsV2sYI5tEn7f3bypOVfJa6kHSqxe1PIQTxirkCQQDjmHOp2JMcltpL+639nnNgZ2U06cRhPHX+tcGTIgoqu1Sqp0bKH7QuUF9WPNWxHrbYY5+s5jnnhTTwQZg74atTAkEAwXelo0JLYHpML7+sLs8aUzitRJXjkW3dY4JPf1wLTNLbawvi4KA/6NA3jx7kCD6KzM7vsWWsRgArrUWa1Dbn6wJARY5pAuZyh1E/I+umEBWl0zemQZaT8tekhBSONWY4zzhzNrhqtQkdau4bROLQuBHX9af0u8WcuroGJMsXOG3OiwJBALc91OPJ8cziaPC80Z/QRvXV877HXTCsZ4lNrnBJxOYxvOMp8eyhu4aOWGE1d/QbEKolwj86tq3ikXvfNmOT0ZsCQBkfviB7CKdHCrUCnpAK0upa9x8uFraomDNxFP/HwTFPSOPGhA5pgAzJgygSu2hpEwFFIwfC3E+pQ2EAhoeIcdw=公鑰:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsAE5TN8kD7U4mFyxBzN1w23Rkf4K8MQ3B0bCZE5crjYp81eUtWrfUM+zLPmF9e1P/ws2yGHvL6mueU9PxtDJn5rSLsQBSxIkN0QB/nq76S4uh2Nrmmrjomejy5LqXnTVbEoIW2RTFBzyMWy4AjQY6P2pAJ8zCagvcdYcweUIqMQIDAQAB 加密前:[object Object] 公鑰加密后:LwggD8SIeWoqzh4gHh/vJ9nEZsqeNZfoxgrRRPD7k0wpp9/uZmR5kfRJ8O59yW5IaOt1z50mJ26ylRBOKNoTTl7Rt4zVmBYX4EXr4Ajq3CINFcPI/j5l8yQRSIgLPUvOxhIAKmfrgNKCaLjSdjK/CnTbPrZoDArI8iAHq/ih4r8= 解密后:{\"username\":\"歡子\",\"password\":123456,\"remark\":\"abcd!@#$:\"} 聯(lián)調(diào)測試自己解密自己加密的的數(shù)據(jù)基本上沒有什么問題,重要的是解密對方加密的數(shù)據(jù)會不會成功,前后端相互加解密的工程中,最重要的就是保持兩邊的加密、填充方式一致、加密位數(shù)一致,還有就是后端Base64字符串轉(zhuǎn)成byte[]數(shù)組的時候要注意,Base64工具類轉(zhuǎn)跟直接字符串getByte()跟用輸入輸出流來轉(zhuǎn),得到的數(shù)組結(jié)果有差異,在本次測試中我也是搞了好久才使得前后端一致,緊跟上面的簡單測試,接下來我們進(jìn)行前后端聯(lián)調(diào)測試 1、AES:前后端相互用對方的key解密對方加密的數(shù)據(jù) 2、RSA:前后端相互用對方的公鑰進(jìn)行加密數(shù)據(jù),然后將數(shù)據(jù)叫給對方解密 這里要講一下步驟,不然大家看不懂下面這幾張圖,為了確保后端加密解密用的是同一個密鑰對,我們采用控制臺輸入前端秘鑰跟前端使用后端公鑰加密后的密文,然后再使用私鑰去解密從而得到前端的明文,而js前端部分,只有不刷新頁面,對象數(shù)據(jù)會存在瀏覽器內(nèi)存中,確保了加密解密是用同一個對密鑰對 總而言之,測試結(jié)果是正確的,接下來就可以再項目中進(jìn)行加解密了 項目應(yīng)用理論思路前、后端的代碼都封裝好了,并且都通過了簡單測試,接下來就是應(yīng)用到項目中,首先我們要解決的是生成公鑰秘鑰并交換的問題,思路如下: 生成: 1、后端:在項目啟動的時候生成RSA公鑰秘鑰并在整個項目運(yùn)行中不發(fā)生改變(或者每隔一段時間更新一次也行),AES的key是每次響應(yīng)之前隨機(jī)獲取 2、前端:我們在訪問頁面開始生成RSA公鑰秘鑰并且希望頁面在刷新之前都不發(fā)生改變,因此將它們存在window對象中(如果需要更加健全,使用H5的本地存儲localStorage、sessionStorage)在head.html中生成,AES的key在每次發(fā)起請求之前隨機(jī)獲取 交換: 1、前端獲取后端RSA公鑰:前端訪問登錄頁面(網(wǎng)站入口),后臺返回modelAndView時注入RSA的公鑰,前端獲取用存到sessionStorage中,直到回話關(guān)閉 2、后端獲取前端RSA公鑰:前端公鑰跟隨http請求發(fā)送到后端 生成與交換公鑰的問題解決了,接下來就是如何傳輸AES加密后的數(shù)據(jù)跟RSA公鑰加密后的AES的key,思路如下: 1、前端:重寫$.ajax方法(或者封裝一個ajax),發(fā)送數(shù)據(jù)前用AES加密數(shù)據(jù)(key隨機(jī)生成),用后端的RSA公鑰加密AES的key,將加密后的data數(shù)據(jù)、加密后的AES的key、前端RSA公鑰發(fā)送到后端;觸發(fā)回調(diào)后,先用前端RSA私鑰解密AES的key,在用明文key去解密 2、后端:寫兩個自定義注解Encrypt、Decrypt,AOP攔截所有帶自定義注解的post請求進(jìn)行加密解密,有@Encrypt需要對返回值進(jìn)行加密,有@Decrypt需要對參數(shù)進(jìn)行解密,加密解密過程與前端的操作同理 前端代碼引入js <!--CryptoJS jsencrypt --> <script th:src="@{/js/cryptojs.js}"></script> <script th:src="@{/js/jsencrypt.js}"></script> <script th:src="@{/js/aesUtil.js}"></script> <script th:src="@{/js/rsaUtil.js}"></script> 下載CryptoJs跟jsencrypt下來發(fā)現(xiàn)CryptoJs需要引入很多js,因此在網(wǎng)上找了這個整合的js,引它就夠了 ![]() 獲取后端公鑰 <script th:inline="javascript"> //獲取后端RSA公鑰并存到sessionStorage sessionStorage.setItem('javaPublicKey', [[${publicKey}]]);</script> 重寫ajax以及生成前端密鑰對,因為我們項目中大部分都是使用$.ajax方法,所有重寫它比較合適,并且我們只攔截post請求 ![]() 后端代碼兩個自定義注解 ![]() ![]() aop掃描所有的controller,攔截帶自定義標(biāo)簽的post請求,進(jìn)行解密、加密再將明文設(shè)置回去(如何使用aop?請戳我之前的博客:SpringBoot系列——aop 面向切面) 要注意的是: 1、我們在aop只設(shè)置了第一個參數(shù),因此controller方法需要是實體接參且第一個參數(shù)就是,所有要求,要么有一個實體Vo參數(shù),要么沒有參數(shù); 2、對于返回值,需要是統(tǒng)一的返回值,因為我們目前是按統(tǒng)一的返回值設(shè)置值的,例如本例中的Result,是我們約定好的統(tǒng)一返回值(后續(xù)升級可以用反射來設(shè)置值); 3、還有一個需要注意的地方,method方法必須是要public修飾的才能設(shè)置方法的形參值,private的設(shè)置不了; PS:2019-06-12補(bǔ)充,我們之前在進(jìn)行jackson序列化和反序列化忘記對date進(jìn)行處理,導(dǎo)致時間格式錯亂,現(xiàn)在補(bǔ)充一下 ![]() 在需要進(jìn)行加密解密的controller方法上加自定義注解,需要加密@Encrypt,需要解密@Decrypt,兩個都有就兩個都加 /** * 登錄 */ @PostMapping("login") @Decrypt @Encrypt public Result<ImsUserVo> login(ImsUserVo userVo, HttpServletResponse response) 到這里就準(zhǔn)備好了,可以發(fā)現(xiàn)除了要在需要進(jìn)行加密解密的controller方法上加自定義注解,根本不需要對之前的代碼進(jìn)行修改,也不影響之前的業(yè)務(wù),當(dāng)然,因為我們直接重寫了$.ajax方法,所以我們需要對所有的post請求的controller都加注解,好在我們用的是單表繼承通用common、自動生成單表基礎(chǔ)增、刪、改、查接口(詳情請戳:SpringBoot系列——Spring-Data-JPA(究極進(jìn)化版) 自動生成單表基礎(chǔ)增、刪、改、查接口),基礎(chǔ)的API我們只需要在common的controller加注解就好了,自定義controller跟重寫的controller也要記得加,接下來就可以把項目跑起來進(jìn)行測試 效果演示未加密之前的效果 數(shù)據(jù)直接暴露在http數(shù)據(jù)包中 AES與RSA混合加密之后的效果 http數(shù)據(jù)包中傳輸?shù)氖敲芪?/p> 解密之后才能看到明文數(shù)據(jù) 到這里我們實現(xiàn)了加解密與項目的結(jié)合,如果項目已經(jīng)是按照我前面說的約定的話,即插即用,不影響項目原有業(yè)務(wù),直接可以使用這一套,把我的代碼拿過去就可以跑起來 后記前后端API交互數(shù)據(jù)加密——AES與RSA混合加密完整實例先記錄到這里,后續(xù)有空再更新升級,雖然說沒有絕對的安全,但加密總比不加密的要好,整篇文章從頭看起來也沒什么,比較簡單,實際上...中間踩了很多坑,心酸血淚史就不在這里闡述了,希望這篇博客能幫到你 2019-09-24補(bǔ)充:完成這個混合加密后不久,我試著將它應(yīng)用在websocket中,而后就有了另一篇文章,感興趣的可以移步前往閱讀《WebSocket數(shù)據(jù)加密——AES與RSA混合加密》 代碼開源2019-09-24補(bǔ)充:混合加密的代碼一直嵌在ims項目里沒有整理出來,而且ims還沒完工尚未開源出來,好在前段時間我開源的一個簡單通用的后臺管理系統(tǒng),Base Admin(開源一套簡單通用的后臺管理系統(tǒng)),里面整合了這套API混合加密,大家前往這個項目查看這套API混合加密代碼 代碼已經(jīng)開源、托管到我的GitHub、碼云: GitHub:https://github.com/huanzi-qch/base-admin 碼云:https:///huanzi-qch/base-admin 中間人攻擊什么是中間人攻擊?維基百科:https://zh./wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB 以下介紹摘自維基百科: 中間人攻擊(英語:Man-in-the-middle attack,縮寫:MITM)在密碼學(xué)和計算機(jī)安全領(lǐng)域中是指攻擊者與通訊的兩端分別創(chuàng)建獨(dú)立的聯(lián)系,并交換其所收到的數(shù)據(jù),使通訊的兩端認(rèn)為他們正在通過一個私密的連接與對方直接對話,但事實上整個會話都被攻擊者完全控制。在中間人攻擊中,攻擊者可以攔截通訊雙方的通話并插入新的內(nèi)容。在許多情況下這是很簡單的(例如,在一個未加密的Wi-Fi 無線接入點的接受范圍內(nèi)的中間人攻擊者,可以將自己作為一個中間人插入這個網(wǎng)絡(luò))。 一個中間人攻擊能成功的前提條件是攻擊者能將自己偽裝成每一個參與會話的終端,并且不被其他終端識破。中間人攻擊是一個(缺乏)相互認(rèn)證的攻擊。大多數(shù)的加密協(xié)議都專門加入了一些特殊的認(rèn)證方法以阻止中間人攻擊。例如,SSL協(xié)議可以驗證參與通訊的一方或雙方使用的證書是否是由權(quán)威的受信任的數(shù)字證書認(rèn)證機(jī)構(gòu)頒發(fā),并且能執(zhí)行雙向身份認(rèn)證。 攻擊示例 中間人攻擊示意圖 假設(shè)愛麗絲(Alice)希望與鮑伯(Bob)通信。同時,馬洛里(Mallory)希望攔截竊會話以進(jìn)行竊聽并可能在某些時候傳送給鮑伯一個虛假的消息。 首先,愛麗絲會向鮑勃索取他的公鑰。如果Bob將他的公鑰發(fā)送給Alice,并且此時馬洛里能夠攔截到這個公鑰,就可以實施中間人攻擊。馬洛里發(fā)送給愛麗絲一個偽造的消息,聲稱自己是鮑伯,并且附上了馬洛里自己的公鑰(而不是鮑伯的)。 愛麗絲收到公鑰后相信這個公鑰是鮑伯的,于是愛麗絲將她的消息用馬洛里的公鑰(愛麗絲以為是鮑伯的)加密,并將加密后的消息回給鮑伯。馬洛里再次截獲愛麗絲回給鮑伯的消息,并使用馬洛里自己的私鑰對消息進(jìn)行解密,如果馬洛里愿意,她也可以對消息進(jìn)行修改,然后馬洛里使用鮑伯原先發(fā)給愛麗絲的公鑰對消息再次加密。當(dāng)鮑伯收到新加密后的消息時,他會相信這是從愛麗絲那里發(fā)來的消息。 1.愛麗絲發(fā)送給鮑伯一條消息,卻被馬洛里截獲:
2.馬洛里將這條截獲的消息轉(zhuǎn)送給鮑伯;此時鮑伯并無法分辨這條消息是否從真的愛麗絲那里發(fā)來的:
3.鮑伯回應(yīng)愛麗絲的消息,并附上了他的公鑰:
4.馬洛里用自己的密鑰替換了消息中鮑伯的密鑰,并將消息轉(zhuǎn)發(fā)給愛麗絲,聲稱這是鮑伯的公鑰:
5.愛麗絲用她以為是鮑伯的公鑰加密了她的消息,以為只有鮑伯才能讀到它:
6.然而,由于這個消息實際上是用馬洛里的密鑰加密的,所以馬洛里可以解密它,閱讀它,并在愿意的時候修改它。他使用鮑伯的密鑰重新加密,并將重新加密后的消息轉(zhuǎn)發(fā)給鮑伯:
7.鮑勃認(rèn)為,這條消息是經(jīng)由安全的傳輸通道從愛麗絲那里傳來的。 這個例子顯示了愛麗絲和鮑伯需要某種方法來確定他們是真正拿到了屬于對方的公鑰,而不是拿到來自攻擊者的公鑰。否則,這類攻擊一般都是可行的,在原理上,可以針對任何使用公鑰——密鑰技術(shù)的通訊消息發(fā)起攻擊。幸運(yùn)的是,有各種不同的技術(shù)可以幫助抵御MITM攻擊。 ----------- end ----------- 單純的加密只能防監(jiān)聽偷窺,不能防中間人偽裝,那么我們應(yīng)該如何阻止中間人攻擊呢?SSL協(xié)議 HTTPS SSL協(xié)議:數(shù)字證書、CA機(jī)構(gòu)、數(shù)字簽名,請看大佬的這篇,通俗易懂:看完這篇文章,我奶奶都懂了https的原理 |
|