Java面試題沖刺第二十六天--實(shí)戰(zhàn)編程
面試題1:你們是怎樣保存用戶密碼等敏感數(shù)據(jù)的?
本題回答參考朱曄的《Java業(yè)務(wù)開發(fā)常見錯(cuò)誤100例》

我們知道,用戶名、密碼、身份證等都屬于用戶敏感信息,其中最敏感的數(shù)據(jù)恐怕就是用戶的密碼了。黑客一旦竊取了用戶密碼,就可以登錄進(jìn)用戶的賬號(hào),消耗其資產(chǎn)、發(fā)布不良信息等;更可怕的是,有些用戶至始至終都是使用一套密碼,密碼一旦泄露,就可以被黑客通過撞庫來登錄全網(wǎng)各大平臺(tái),嘿嘿嘿。
為了防止密碼泄露,最重要的原則是不要在數(shù)據(jù)庫保存用戶原始密碼。
大家經(jīng)常說,不要明文保存用戶密碼,應(yīng)該把密碼通過 MD5 加密后保存。這的確是一個(gè)正確的方向,但這個(gè)說法并不準(zhǔn)確。
首先,MD5 其實(shí)不是真正的加密算法。所謂加密算法,是可以使用密鑰把明文加密為密文,隨后還可以使用密鑰解密出明文,是雙向的。而 MD5 是散列、哈希算法或者摘要算法。不管多長(zhǎng)的數(shù)據(jù),使用 MD5 運(yùn)算后得到的都是固定長(zhǎng)度的摘要信息或指紋信息,無法再解密為原始數(shù)據(jù)。所以,MD5 是單向的。最重要的是,僅僅使用 MD5 對(duì)密碼進(jìn)行摘要,并不安全。
比如,使用如下代碼在保持用戶信息時(shí),對(duì)密碼進(jìn)行了 MD5 計(jì)算:
UserData userData = new UserData(); userData.setId(1L); userData.setName(name); //密碼字段使用MD5哈希后保存 userData.setPassword(DigestUtils.md5Hex(password)); return userRepository.save(userData);
通過輸出,可以看到密碼是 32 位的 MD5:
"password": "325a2cc052914ceeb8c19016c091d2ac"
然后,再去破解網(wǎng)站試一下這個(gè) MD5,就可以得到原始密碼是 salt,也就知道了鹽值是 salt:
其實(shí),知道鹽是什么沒什么關(guān)系,關(guān)鍵的是我們是在代碼里寫死了鹽,并且鹽很短、所有用戶都是這個(gè)鹽。這么做就會(huì)有三個(gè)問題:
- 因?yàn)辂}太短、太簡(jiǎn)單了,如果用戶原始密碼也很簡(jiǎn)單,那么整個(gè)拼起來的密碼也很短,這樣一般的 MD5 破解網(wǎng)站都可以直接解密這個(gè) MD5,除去鹽就是原始密碼了。
- 相同的鹽,意味著使用相同密碼的用戶 MD5 值是一樣的,知道了一個(gè)用戶的密碼就可能知道了多個(gè)。
- 黑客也可以使用這個(gè)鹽來構(gòu)建一張彩虹表,也就是字典表,雖然會(huì)花不少代價(jià),但是一旦構(gòu)建完成,所有人的密碼都可以被破解。
所以,最好是每一個(gè)密碼都有獨(dú)立的鹽,并且鹽要長(zhǎng)一點(diǎn),比如超過 20 位。
第二,雖然說每個(gè)人的鹽最好不同,但也不建議將一部分用戶數(shù)據(jù)作為鹽。比如,使用用戶名作為鹽:
userData.setPassword(DigestUtils.md5Hex(name + password));
如果世界上所有的系統(tǒng)都是按照這個(gè)方案來保存密碼,那么 root、admin 這樣的用戶使用再?gòu)?fù)雜的密碼也總有一天會(huì)被破解,因?yàn)楹诳蛡兺耆梢葬槍?duì)這些常用用戶名來做彩虹表。所以,鹽最好是隨機(jī)的值,并且是全球唯一的,意味著全球不可能有現(xiàn)成的彩虹表給你用。
正確的做法是,使用全球唯一的、和用戶無關(guān)的、足夠長(zhǎng)的隨機(jī)值作為鹽。比如,可以使用 UUID 作為鹽,把鹽一起保存到數(shù)據(jù)庫中:
userData.setSalt(UUID.randomUUID().toString());
userData.setPassword(DigestUtils.md5Hex(userData.getSalt() + password));
并且每次用戶修改密碼的時(shí)候都重新計(jì)算鹽,重新保存新的密碼。
需要注意的是,這么做雖然黑客已經(jīng)很難通過彩虹表來破解密碼了,但是仍然有可能暴力破解密碼,也就是對(duì)于同一個(gè)用戶名使用常見的密碼逐一嘗試登錄。因此,除了做好密碼哈希保存的工作外,我們還要建設(shè)一套完善的安全防御機(jī)制,在感知到暴力破解危害的時(shí)候,開啟短信驗(yàn)證、圖形驗(yàn)證碼、賬號(hào)暫時(shí)鎖定等防御機(jī)制來抵御暴力破解。
那么姓名和身份證又是怎么保存的?
我們把姓名和身份證,叫做二要素。
現(xiàn)在互聯(lián)網(wǎng)非常發(fā)達(dá),很多服務(wù)都可以在網(wǎng)上辦理,很多網(wǎng)站僅僅依靠二要素來確認(rèn)你是誰。所以,二要素是比較敏感的數(shù)據(jù),如果在數(shù)據(jù)庫中明文保存,那么數(shù)據(jù)庫被攻破后,黑客就可能拿到大量的二要素信息。如果這些二要素被用來申請(qǐng)貸款等,后果不堪設(shè)想。
之前我們提到的單向散列算法(MD5),顯然不適合用來加密保存二要素,因?yàn)閿?shù)據(jù)無法解密。這個(gè)時(shí)候,我們需要選擇真正的加密算法??晒┻x擇的算法,包括對(duì)稱加密和非對(duì)稱加密算法兩類。
- 對(duì)稱加密算法:是使用相同的密鑰進(jìn)行加密和解密。使用對(duì)稱加密算法來加密雙方的通信的話,雙方需要先約定一個(gè)密鑰,加密方才能加密,接收方才能解密。如果密鑰在發(fā)送的時(shí)候被竊取,那么加密就是白忙一場(chǎng)。因此,這種加密方式的特點(diǎn)是,加密速度比較快,但是密鑰傳輸分發(fā)有泄露風(fēng)險(xiǎn)。

- 非對(duì)稱加密算法:或者叫公鑰密碼算法。公鑰密碼是由一對(duì)密鑰對(duì)構(gòu)成的,使用公鑰或者說加密密鑰來加密,使用私鑰或者說解密密鑰來解密,公鑰可以任意公開,私鑰不能公開。使用非對(duì)稱加密的話,通信雙方可以僅分享公鑰用于加密,加密后的數(shù)據(jù)沒有私鑰無法解密。因此,這種加密方式的特點(diǎn)是,加密速度比較慢,但是解決了密鑰的配送分發(fā)安全問題。

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

面試題2:怎么控制用戶請(qǐng)求的冪等性的?
冪等性:對(duì)于同一筆業(yè)務(wù)操作,不管調(diào)用多少次,得到的結(jié)果都是一樣的。
當(dāng)然,有些操作是天然冪等的,如:
- 查詢操作:查詢一次和查詢多次,在數(shù)據(jù)不變的情況下,查詢結(jié)果是一樣的。select是天然的冪等操作;
- 刪除操作:刪除操作也是冪等的,刪除一次和多次刪除都是把數(shù)據(jù)刪除。
后臺(tái)控制冪等性的幾種途徑:
1.設(shè)置唯一索引:防止新增臟數(shù)據(jù)
比如支付寶的資金賬戶,支付寶也有用戶賬戶,每個(gè)用戶只能有一個(gè)資金賬戶,怎么防止給用戶創(chuàng)建資金賬戶多個(gè),那么給資金賬戶表中的用戶ID加唯一索引,所以一個(gè)用戶新增成功一個(gè)資金賬戶記錄。要點(diǎn):唯一索引或唯一組合索引來防止新增數(shù)據(jù)存在臟數(shù)據(jù)(當(dāng)表存在唯一索引,并發(fā)時(shí)新增報(bào)錯(cuò)時(shí),再查詢一次就可以了,數(shù)據(jù)應(yīng)該已經(jīng)存在了,返回結(jié)果即可);
2.token機(jī)制:防止頁面重復(fù)提交
原理上通過session token來實(shí)現(xiàn)的(也可以通過redis來實(shí)現(xiàn))。當(dāng)客戶端請(qǐng)求頁面時(shí),服務(wù)器會(huì)生成一個(gè)隨機(jī)數(shù)Token,并且將Token放置到session當(dāng)中,然后將Token發(fā)給客戶端(一般通過構(gòu)造hidden表單)。下次客戶端提交請(qǐng)求時(shí),Token會(huì)隨著表單一起提交到服務(wù)器端。
服務(wù)器端第一次驗(yàn)證相同過后,會(huì)將session中的Token值更新下,若用戶重復(fù)提交,第二次的驗(yàn)證判斷將失敗,因?yàn)橛脩籼峤坏谋韱沃械腡oken沒變,但服務(wù)器端session中Token已經(jīng)改變了。
3.悲觀鎖
獲取數(shù)據(jù)的時(shí)候加鎖獲取。
select * from table_xxx where id='xxx' for update;
注意:id字段一定是主鍵或者唯一索引,不然是鎖表,會(huì)死人的;悲觀鎖使用時(shí)一般伴隨事務(wù)一起使用,數(shù)據(jù)鎖定時(shí)間可能會(huì)很長(zhǎng),根據(jù)實(shí)際情況選用;
4.樂觀鎖
樂觀鎖只是在更新數(shù)據(jù)那一刻鎖表,其他時(shí)間不鎖表,所以相對(duì)于悲觀鎖,效率更高。樂觀鎖的實(shí)現(xiàn)方式多種多樣,可以通過version或者其他狀態(tài)條件判斷:
- 通過版本號(hào)實(shí)現(xiàn)
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.分布式鎖
如果是分布式系統(tǒng),構(gòu)建全局唯一索引比較困難,例如唯一性的字段沒法確定,這時(shí)候可以引入分布式鎖,通過第三方的系統(tǒng)(redis或zookeeper),在業(yè)務(wù)系統(tǒng)插入數(shù)據(jù)或者更新數(shù)據(jù),獲取分布式鎖,然后做操作,之后釋放鎖,這樣其實(shí)是把多線程并發(fā)的鎖的思路,引入多多個(gè)系統(tǒng),也就是分布式系統(tǒng)中得解決思路。
要點(diǎn):某個(gè)長(zhǎng)流程處理過程要求不能并發(fā)執(zhí)行,可以在流程執(zhí)行之前根據(jù)某個(gè)標(biāo)志(用戶ID+后綴等)獲取分布式鎖,其他流程執(zhí)行時(shí)獲取鎖就會(huì)失敗,也就是同一時(shí)間該流程只能有一個(gè)能執(zhí)行成功,執(zhí)行完成后,釋放分布式鎖(分布式鎖要第三方系統(tǒng)提供);
面試題3:你們是如何預(yù)防SQL注入問題的?
SQL注入攻擊的總體思路
- 尋找到SQL注入的位置
- 判斷服務(wù)器類型和后臺(tái)數(shù)據(jù)庫類型
- 針對(duì)不通的服務(wù)器和數(shù)據(jù)庫特點(diǎn)進(jìn)行SQL注入攻擊
預(yù)防方式:
1、PreparedStatement(簡(jiǎn)單有效)
采用預(yù)編譯語句集,它內(nèi)置了處理SQL注入的能力,只要使用它的setXXX方法傳值即可。
sql注入只對(duì)sql語句的準(zhǔn)備(編譯)過程有破壞作用,而PreparedStatement在執(zhí)行階段只是把輸入串作為數(shù)據(jù)處理,不再對(duì)sql語句進(jìn)行解析,因此也就避免了sql注入問題。
2、使用正則表達(dá)式過濾傳入的參數(shù)
要引入的包:
import java.util.regex.*;
正則表達(dá)式:
private String CHECKSQL = “^(.+)\\sand\\s(.+)|(.+)\\sor(.+)\\s$”;
判斷是否匹配:
Pattern.matches(CHECKSQL,targerStr);
下面是常用來過濾參數(shù)是否存在SQL注入的正則表達(dá)式:
- 檢測(cè)SQL meta-characters的正則表達(dá)式 : /(\%27)|(\')|(\-\-)|(\%23)|(#)/ix
- 修正檢測(cè)SQL meta-characters的正則表達(dá)式 : /((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(:))/i
- 典型的SQL 注入攻擊的正則表達(dá)式 : /\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/ix
- 檢測(cè)SQL注入,UNION查詢關(guān)鍵字的正則表達(dá)式 : /((\%27)|(\'))union/ix(\%27)|(\')
3.使用正則表達(dá)式過濾傳入的URL
比較通用的一個(gè)方法,jsp中調(diào)用該函數(shù)檢查是否包函非法字符,防止SQL從URL注入:
(||之間的參數(shù)可以根據(jù)自己程序的需要添加)
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;
}
總結(jié)
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java設(shè)計(jì)模式之單例模式Singleton Pattern詳解
這篇文章主要介紹了Java設(shè)計(jì)模式之單例模式Singleton Pattern詳解,一些常用的工具類、線程池、緩存,數(shù)據(jù)庫,數(shù)據(jù)庫連接池、賬戶登錄系統(tǒng)、配置文件等程序中可能只允許我們創(chuàng)建一個(gè)對(duì)象,這就需要單例模式,需要的朋友可以參考下2023-12-12
Java正則驗(yàn)證字串符RegexValidator類使用
正則驗(yàn)證字串符是一種強(qiáng)大的工具,可以幫助程序員在處理字符串時(shí)輕松進(jìn)行復(fù)雜匹配,本文將介紹正則表達(dá)式的概念、語法和在編程中的應(yīng)用,并通過實(shí)例演示如何使用正則表達(dá)式進(jìn)行字符串匹配、替換和提取等操作2023-11-11
SpringBoot+Ant Design Vue實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出功能方式
這篇文章主要介紹了SpringBoot+Ant Design Vue實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出功能方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
JavaFX實(shí)現(xiàn)簡(jiǎn)易時(shí)鐘效果
這篇文章主要為大家詳細(xì)介紹了JavaFX實(shí)現(xiàn)簡(jiǎn)易時(shí)鐘效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
Spring Cloud Gateway打造可擴(kuò)展的微服務(wù)網(wǎng)關(guān)
微服務(wù)網(wǎng)關(guān)是一個(gè)位于客戶端和后端微服務(wù)之間的服務(wù)器,用于處理所有與客戶端的通信,Spring Cloud Gateway都是一個(gè)值得考慮的選擇,它將幫助您更好地管理和保護(hù)您的微服務(wù),感興趣的朋友一起看看吧2023-11-11
SpringBoot如何使用TestEntityManager進(jìn)行JPA集成測(cè)試
TestEntityManager是Spring Framework提供的一個(gè)測(cè)試框架,它可以幫助我們進(jìn)行 JPA 集成測(cè)試,在本文中,我們將介紹如何使用 TestEntityManager 進(jìn)行 JPA 集成測(cè)試,感興趣的跟著小編一起來學(xué)習(xí)吧2023-06-06

