Java面試題沖刺第二十六天–實戰編程

面試題1:你們是怎樣保存用戶密碼等敏感數據的?

本題回答參考朱曄的《Java業務開發常見錯誤100例》

在這裡插入圖片描述

我們知道,用戶名、密碼、身份證等都屬於用戶敏感信息,其中最敏感的數據恐怕就是用戶的密碼瞭。黑客一旦竊取瞭用戶密碼,就可以登錄進用戶的賬號,消耗其資產、發佈不良信息等;更可怕的是,有些用戶至始至終都是使用一套密碼,密碼一旦泄露,就可以被黑客通過撞庫來登錄全網各大平臺,嘿嘿嘿。

為瞭防止密碼泄露,最重要的原則是不要在數據庫保存用戶原始密碼。

大傢經常說,不要明文保存用戶密碼,應該把密碼通過 MD5 加密後保存。這的確是一個正確的方向,但這個說法並不準確。

首先,MD5 其實不是真正的加密算法。所謂加密算法,是可以使用密鑰把明文加密為密文,隨後還可以使用密鑰解密出明文,是雙向的。而 MD5 是散列、哈希算法或者摘要算法。不管多長的數據,使用 MD5 運算後得到的都是固定長度的摘要信息或指紋信息,無法再解密為原始數據。所以,MD5 是單向的。最重要的是,僅僅使用 MD5 對密碼進行摘要,並不安全。

比如,使用如下代碼在保持用戶信息時,對密碼進行瞭 MD5 計算:

UserData userData = new UserData();
userData.setId(1L);
userData.setName(name);
//密碼字段使用MD5哈希後保存
userData.setPassword(DigestUtils.md5Hex(password));
return userRepository.save(userData);

通過輸出,可以看到密碼是 32 位的 MD5:

“password”: “325a2cc052914ceeb8c19016c091d2ac”

然後,再去破解網站試一下這個 MD5,就可以得到原始密碼是 salt,也就知道瞭鹽值是 salt:

其實,知道鹽是什麼沒什麼關系,關鍵的是我們是在代碼裡寫死瞭鹽,並且鹽很短、所有用戶都是這個鹽。這麼做就會有三個問題:

  • 因為鹽太短、太簡單瞭,如果用戶原始密碼也很簡單,那麼整個拼起來的密碼也很短,這樣一般的 MD5 破解網站都可以直接解密這個 MD5,除去鹽就是原始密碼瞭。
  • 相同的鹽,意味著使用相同密碼的用戶 MD5 值是一樣的,知道瞭一個用戶的密碼就可能知道瞭多個。
  • 黑客也可以使用這個鹽來構建一張彩虹表,也就是字典表,雖然會花不少代價,但是一旦構建完成,所有人的密碼都可以被破解。

所以,最好是每一個密碼都有獨立的鹽,並且鹽要長一點,比如超過 20 位。

第二,雖然說每個人的鹽最好不同,但也不建議將一部分用戶數據作為鹽。比如,使用用戶名作為鹽:

userData.setPassword(DigestUtils.md5Hex(name + password));

如果世界上所有的系統都是按照這個方案來保存密碼,那麼 root、admin 這樣的用戶使用再復雜的密碼也總有一天會被破解,因為黑客們完全可以針對這些常用用戶名來做彩虹表。所以,鹽最好是隨機的值,並且是全球唯一的,意味著全球不可能有現成的彩虹表給你用。

正確的做法是,使用全球唯一的、和用戶無關的、足夠長的隨機值作為鹽。比如,可以使用 UUID 作為鹽,把鹽一起保存到數據庫中:

userData.setSalt(UUID.randomUUID().toString());
userData.setPassword(DigestUtils.md5Hex(userData.getSalt() + password));

並且每次用戶修改密碼的時候都重新計算鹽,重新保存新的密碼。

需要註意的是,這麼做雖然黑客已經很難通過彩虹表來破解密碼瞭,但是仍然有可能暴力破解密碼,也就是對於同一個用戶名使用常見的密碼逐一嘗試登錄。因此,除瞭做好密碼哈希保存的工作外,我們還要建設一套完善的安全防禦機制,在感知到暴力破解危害的時候,開啟短信驗證、圖形驗證碼、賬號暫時鎖定等防禦機制來抵禦暴力破解。

那麼姓名和身份證又是怎麼保存的?

我們把姓名和身份證,叫做二要素。

現在互聯網非常發達,很多服務都可以在網上辦理,很多網站僅僅依靠二要素來確認你是誰。所以,二要素是比較敏感的數據,如果在數據庫中明文保存,那麼數據庫被攻破後,黑客就可能拿到大量的二要素信息。如果這些二要素被用來申請貸款等,後果不堪設想。

之前我們提到的單向散列算法(MD5),顯然不適合用來加密保存二要素,因為數據無法解密。這個時候,我們需要選擇真正的加密算法。可供選擇的算法,包括對稱加密和非對稱加密算法兩類。

  • 對稱加密算法:是使用相同的密鑰進行加密和解密。使用對稱加密算法來加密雙方的通信的話,雙方需要先約定一個密鑰,加密方才能加密,接收方才能解密。如果密鑰在發送的時候被竊取,那麼加密就是白忙一場。因此,這種加密方式的特點是,加密速度比較快,但是密鑰傳輸分發有泄露風險。

在這裡插入圖片描述

  • 非對稱加密算法:或者叫公鑰密碼算法。公鑰密碼是由一對密鑰對構成的,使用公鑰或者說加密密鑰來加密,使用私鑰或者說解密密鑰來解密,公鑰可以任意公開,私鑰不能公開。使用非對稱加密的話,通信雙方可以僅分享公鑰用於加密,加密後的數據沒有私鑰無法解密。因此,這種加密方式的特點是,加密速度比較慢,但是解決瞭密鑰的配送分發安全問題。

在這裡插入圖片描述

但是,對於保存敏感信息的場景來說,加密和解密都是我們的服務端程序,不太需要考慮密鑰的分發安全性,也就是說使用非對稱加密算法沒有太大的意義。我們一般會使用對稱加密算法來加密數據。常見的對稱加密算法有:DES、3DES 和 AES。

在這裡插入圖片描述

面試題2:怎麼控制用戶請求的冪等性的?

冪等性:對於同一筆業務操作,不管調用多少次,得到的結果都是一樣的。

當然,有些操作是天然冪等的,如:

  • 查詢操作:查詢一次和查詢多次,在數據不變的情況下,查詢結果是一樣的。select是天然的冪等操作;
  • 刪除操作:刪除操作也是冪等的,刪除一次和多次刪除都是把數據刪除。

後臺控制冪等性的幾種途徑:

1.設置唯一索引:防止新增臟數據

比如支付寶的資金賬戶,支付寶也有用戶賬戶,每個用戶隻能有一個資金賬戶,怎麼防止給用戶創建資金賬戶多個,那麼給資金賬戶表中的用戶ID加唯一索引,所以一個用戶新增成功一個資金賬戶記錄。要點:唯一索引或唯一組合索引來防止新增數據存在臟數據(當表存在唯一索引,並發時新增報錯時,再查詢一次就可以瞭,數據應該已經存在瞭,返回結果即可);

2.token機制:防止頁面重復提交

原理上通過session token來實現的(也可以通過redis來實現)。當客戶端請求頁面時,服務器會生成一個隨機數Token,並且將Token放置到session當中,然後將Token發給客戶端(一般通過構造hidden表單)。下次客戶端提交請求時,Token會隨著表單一起提交到服務器端。

服務器端第一次驗證相同過後,會將session中的Token值更新下,若用戶重復提交,第二次的驗證判斷將失敗,因為用戶提交的表單中的Token沒變,但服務器端session中Token已經改變瞭。

3.悲觀鎖

獲取數據的時候加鎖獲取。

select * from table_xxx where id=‘xxx’ for update;

註意:id字段一定是主鍵或者唯一索引,不然是鎖表,會死人的;悲觀鎖使用時一般伴隨事務一起使用,數據鎖定時間可能會很長,根據實際情況選用;

4.樂觀鎖

樂觀鎖隻是在更新數據那一刻鎖表,其他時間不鎖表,所以相對於悲觀鎖,效率更高。樂觀鎖的實現方式多種多樣,可以通過version或者其他狀態條件判斷:

  • 通過版本號實現

update table_xxx set name=#name#,version=version+1 where version=#version#;

  • 通過條件限制

update table_xxx set avai_amount=avai_amount where avai_amount >= 0

5.分佈式鎖

如果是分佈式系統,構建全局唯一索引比較困難,例如唯一性的字段沒法確定,這時候可以引入分佈式鎖,通過第三方的系統(redis或zookeeper),在業務系統插入數據或者更新數據,獲取分佈式鎖,然後做操作,之後釋放鎖,這樣其實是把多線程並發的鎖的思路,引入多多個系統,也就是分佈式系統中得解決思路。

要點:某個長流程處理過程要求不能並發執行,可以在流程執行之前根據某個標志(用戶ID+後綴等)獲取分佈式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程隻能有一個能執行成功,執行完成後,釋放分佈式鎖(分佈式鎖要第三方系統提供);

面試題3:你們是如何預防SQL註入問題的?

SQL註入攻擊的總體思路

  • 尋找到SQL註入的位置
  • 判斷服務器類型和後臺數據庫類型
  • 針對不通的服務器和數據庫特點進行SQL註入攻擊

預防方式:

1、PreparedStatement(簡單有效)

采用預編譯語句集,它內置瞭處理SQL註入的能力,隻要使用它的setXXX方法傳值即可。

sql註入隻對sql語句的準備(編譯)過程有破壞作用,而PreparedStatement在執行階段隻是把輸入串作為數據處理,不再對sql語句進行解析,因此也就避免瞭sql註入問題。

2、使用正則表達式過濾傳入的參數

要引入的包:

import java.util.regex.*;

正則表達式:

private String CHECKSQL =^(.+)\\sand\\s(.+)|(.+)\\sor(.+)\\s$”;

判斷是否匹配:

Pattern.matches(CHECKSQL,targerStr);

下面是常用來過濾參數是否存在SQL註入的正則表達式:

  • 檢測SQL meta-characters的正則表達式 : /(\%27)|(\’)|(\-\-)|(\%23)|(#)/ix
  • 修正檢測SQL meta-characters的正則表達式 : /((\%3D)|(=))[^\n]*((\%27)|(\’)|(\-\-)|(\%3B)|(:))/i
  • 典型的SQL 註入攻擊的正則表達式 : /\w*((\%27)|(\’))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/ix
  • 檢測SQL註入,UNION查詢關鍵字的正則表達式 : /((\%27)|(\’))union/ix(\%27)|(\’)

3.使用正則表達式過濾傳入的URL

比較通用的一個方法,jsp中調用該函數檢查是否包函非法字符,防止SQL從URL註入:

(||之間的參數可以根據自己程序的需要添加)

public static boolean sql_inj(String str){
    String inj_str = "'|and|exec|insert|select|delete|update|
    count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+|,";
    String inj_stra[] = split(inj_str,"|");
    for (int i=0 ; i < inj_stra.length ; i++ ){
        if (str.indexOf(inj_stra[i])>=0){
        return true;
        }
    }
    return false;
}

總結

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: