Restful介面安全重要性
現在網際網路開發的框架越來越豐富,大多數的系統架構也都是朝著微服務化,雲原生化演進。而且自從中臺的概念提出之後,各大公司也開始拆分自己的技術中臺,抽離出一些公共的技術服務中臺。
這使得我們服務開發也越來越容易,較少的精力放在框架層處理,更多關注業務邏輯的實現。現在系統大都是異構化,服務之間通過 Http 或者 Rpc 進行遠端呼叫。當我們的後臺介面暴露給前端或者移動 app 端時就要考慮介面的安全性。
例如,現在的 app 登陸或者重置密碼介面都需要傳送簡訊驗證碼,如果我們的傳送介面被他人利用,不僅容易出現簡訊盜刷,還有可能影響我們正常的服務使用。所以我們在設計介面時可以從以下幾個方面加上介面安全性相關的設計。
簽名校驗生成 appId 和 appKey首先,我們可以為每個請求呼叫方生成固定長度的隨機字串 appId 和 appKey,可以使用 RandomUtil 方法。
“Talk is cheap,show me your code.”
當然有很多其他的隨機字串生成方法,比如用機器 id + timestamp 時間戳方式,這裡只是簡單的列舉一種隨機方法。
public static void main(String[] args) { String appId = RandomUtil.randomString(20); String chars = "$&@?<>~!%#"; StringBuilder appKeyTemp = new StringBuilder(RandomUtil.randomString(20)); for(int i=0;i<chars.length();i++){ int index = new Random().nextInt(chars.length()); appKeyTemp.insert(index, chars.charAt(index)); } System.out.println("appId:"+appId+" appKey:"+appKeyTemp.toString());}
appId:tow9e8piyuj23w5e46yj appKey:$$$8?ze><<>zuq%8ba#dqmr8dwx1jy
生成 appId 和 appKey 之後可以將其放在配置檔案或者配置中心,通過 @Value 註解獲取變數值,在介面呼叫時進行校驗。
@Componentpublic class EncryptUtils{ @Value("${appId}") String appId; @Value("${appKey}") String appKey; /** * 判斷 appId 和 appKey 是否校驗 * * @param appId app id. * @param appKey app key. * @return result. */ public boolean appIdMatch(String appId, String appKey) { return this.appId.equals(appId) && this.appKey.equals(appKey); }}
介面簽名校驗 sign但是以上生成的 appId 和 appKey 還是放在請求引數或者請求包頭,通過代理抓包還是能夠拿到這兩個引數。所以,為了安全我們還必須對請求呼叫方做簽名驗證。生成簽名的方式主要是對稱加密,這裡展示了 AES256 加密方法。
我們可以將 appId_appKey_timestamp 作為字串,用 AES/ECB/PKCS5Padding 演算法填充,AES256 演算法加密,由於 AES 是對稱加密,所以只需要根據請求引數再做一次加密,然後比較加密之後的密文和請求的 sign 是否一致即可以達到校驗介面請求的目的。
加密需要前後端約定金鑰,AES 加密金鑰長度為 16 位,這點需要注意。加密方法如下
/** * * @param content 加密的字串 * @param encryptKey key 值 * @return result. * @throws Exception IllegalBlockSizeException */ private String encryptAppId(String content, String encryptKey) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(256); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES")); byte[] b = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); // base64 進行轉碼 return Base64.encodeBase64String(b); }/** * 對 appId 生成簽名 * @param appId app id. * @param appKey app key. * @param timestamp 時間戳 * @param encryptKey 加密金鑰串 * @return result. */ public String signAppId(String appId, String appKey, Long timestamp, String encryptKey) throws Exception { return this.encryptAppId(appId + "_" + appKey + "_" + timestamp, encryptKey); } public static void main(String[] args){ String appId = "tow9e8piyuj23w5e46yj"; String appKey = "$$$8?ze><<>zuq%8ba#dqmr8dwx1jy"; Long timestamp = 1600143987258; String encryptKey = "vcko6mqa2oucmuva"; System.out.println("sign: "signAppId(appId,appKey,timestamp,encryptKey)) }
sign: Uw6yqZKmHZPPMPmDDKNiNXNOHM8RbVJ/aeI/og4Ex0wK48wKxxitjE0Gh3AabQZrU3JwqWRb6Wr8+ZaHKYgHrQ==
當呼叫方呼叫時傳入 sign 和 timestamp 欄位,這樣我們在介面呼叫時可以 sign 欄位進行簽名校驗。在攻擊方不知道加密金鑰時,基本上很難偽造簽名信息。介面校驗是每個介面都需要驗證的,所以我們可以定義公共切面方法 對所有介面進行攔截並校驗。
介面切面攔截器首先,引入 maven 包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>
定義切面
/** * 定義切點 */@Pointcut("execution(public * com.test.controller..*(..))")public void appIdSign() {}/** * 執行校驗方法 * * @param joinPoint 織入點 * @throws Exception 加密異常 */ @Before("appIdSign()") public void doVilaSign(JoinPoint joinPoint) throws Exception { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 獲取簽名 String appId = request.getHeader("appId"); String appKey = request.getHeader("appKey"); String sign = request.getHeader("sign"); String timestamp = request.getHeader("timestamp"); logger.debug("appId:[{}],appKey:[{}],sign:[{}],timestamp:[{}]",appId, appKey, sign, timestamp); if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(appKey) || StringUtils.isEmpty(sign)|| StringUtils.isEmpty(timestamp) || !encryptUtils.appIdMatch(appId, appKey)) { throw new Exception("簽名校驗失敗"); } String signValid = encryptUtils.signAppId(timestamp); if (!sign.equals(signValid)) { throw new Exception("簽名校驗失敗"); }}
以上,可以做到對 controller 包下的所有介面方法攔截,並獲取請求頭引數,通過請求引數生成 sign 簽名信息並校驗。
雖然以上可以做到簽名校驗,但是,當別人抓包拿到請求頭和請求引數,再重複呼叫我們的介面,還是會有安全問題。所以還需要對介面防重放做攔截。
介面防重基於時間戳一般呼叫方到後臺的介面時間都遠小於 30s(正常網路情況)。同時,攻擊者抓包並偽造請求時間一般在 30s 之上。在之前我們約定了請求頭資訊包含 timestamp,所以我們可以限制超過當前時間 30s 的請求。
long now = System.currentTimeMillis();//介面防重防,30s 有效if ((Long.parseLong(timestamp)) + 30 * 1000 < now) { throw new Exception("簽名校驗失敗");}
基於時間戳+隨機數
以上時間限制還是給攻擊者留了 30s 的偽造時間,為了絕對安全,常用的防止重放的機制是使用 timestamp 和 nonce 來做的重放機制。
nonce 是客戶端呼叫方生成的隨機數,在呼叫端將 timestamp + nonce 作為 key 儲存在 redis 中,當客戶端第一次呼叫時先去 redis 查詢是否有該 key 資訊,如果沒有則快取並設定過期時間 (30s),如果有則拒絕該請求(非法重複請求)。
https 加密當然,最基礎的安全加密是介面採用 https 協議
http 和 https 的區別https 可以視為 http+SSL 安全套接層,https 協議需要到 ca 申請證書,客戶端和服務端握手流程大致如下:
the TLS Handshake
圖片來源:https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/
在傳統的 HTTP 三次握手之上增加了 SSL 加密認證的過程,保證了資料傳輸的安全性。所以條件允許的情況下,儘量採用 https 介面呼叫。
最後“魔高一尺 道高一丈”
安全無小事,凡事都沒有萬能之法,所以我們在介面設計時需要嚴謹考慮安全性,儘可能做到加密,驗籤,防篡改。