Spring Boot 中密碼加密的兩種姿式!

先說一句:密碼是沒法解密的。你們也不要再問鬆哥微人事項目中的密碼怎麼解密了!前端

密碼沒法解密,仍是爲了確保系統安全。今天鬆哥就來和你們聊一聊,密碼要如何處理,才能在最大程度上確保咱們的系統安全。vue

本文是 Spring Security 系列的第 20 篇,閱讀本系列前面的文章有助於更好的理解本文:java

  1. 挖一個大坑,Spring Security 開搞!
  2. 鬆哥手把手帶你入門 Spring Security,別再問密碼怎麼解密了
  3. 手把手教你定製 Spring Security 中的表單登陸
  4. Spring Security 作先後端分離,咱就別作頁面跳轉了!通通 JSON 交互
  5. Spring Security 中的受權操做原來這麼簡單
  6. Spring Security 如何將用戶數據存入數據庫?
  7. Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!
  8. Spring Boot + Spring Security 實現自動登陸功能
  9. Spring Boot 自動登陸,安全風險要怎麼控制?
  10. 在微服務項目中,Spring Security 比 Shiro 強在哪?
  11. SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)
  12. Spring Security 中如何快速查看登陸用戶 IP 地址等信息?
  13. Spring Security 自動踢掉前一個登陸用戶,一個配置搞定!
  14. Spring Boot + Vue 先後端分離項目,如何踢掉已登陸用戶?
  15. Spring Security 自帶防火牆!你都不知道本身的系統有多安全!
  16. 什麼是會話固定攻擊?Spring Boot 中要如何防護會話固定攻擊?
  17. 集羣化部署,Spring Security 要如何處理 session 共享?
  18. 鬆哥手把手教你在 SpringBoot 中防護 CSRF 攻擊!so easy!
  19. 要學就學透徹!Spring Security 中 CSRF 防護源碼解析

1.爲何要加密

2011 年 12 月 21 日,有人在網絡上公開了一個包含 600 萬個 CSDN 用戶資料的數據庫,數據所有爲明文儲存,包含用戶名、密碼以及註冊郵箱。事件發生後 CSDN 在微博、官方網站等渠道發出了聲明,解釋說此數據庫系 2009 年備份所用,因不明緣由泄露,已經向警方報案,後又在官網發出了公開道歉信。在接下來的十多天裏,金山、網易、京東、噹噹、新浪等多家公司被捲入到此次事件中。整個事件中最觸目驚心的莫過於 CSDN 把用戶密碼明文存儲,因爲不少用戶是多個網站共用一個密碼,所以一個網站密碼泄露就會形成很大的安全隱患。因爲有了這麼多前車可鑑,咱們如今作系統時,密碼都要加密處理。git

此次泄密,也留下了一些有趣的事情,特別是對於廣大程序員設置密碼這一項。人們從 CSDN 泄密的文件中,發現了一些好玩的密碼,例如以下這些:程序員

  • ppnn13%dkstFeb.1st 這段密碼的中文解析是:娉娉嫋嫋十三餘,豆蔻梢頭二月初。
  • csbt34.ydhl12s 這段密碼的中文解析是:池上碧苔三四點,葉底黃鸝一兩聲
  • ...

等等不一而足,你會發現不少程序員的人文素養仍是很是高的,讓人嘖嘖稱奇。github

2.加密方案

密碼加密咱們通常會用到散列函數,又稱散列算法、哈希函數,這是一種從任何數據中建立數字「指紋」的方法。算法

散列函數把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定下來,而後將數據打亂混合,從新建立一個散列值。散列值一般用一個短的隨機字母和數字組成的字符串來表明。好的散列函數在輸入域中不多出現散列衝突。在散列表和數據處理中,不抑制衝突來區別數據,會使得數據庫記錄更難找到。數據庫

咱們經常使用的散列函數有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。後端

可是僅僅使用散列函數還不夠,單純的只使用散列函數,若是兩個用戶密碼明文相同,生成的密文也會相同,這樣就增長的密碼泄漏的風險。安全

爲了增長密碼的安全性,通常在密碼加密過程當中還須要加鹽,所謂的鹽能夠是一個隨機數也能夠是用戶名,加鹽以後,即便密碼明文相同的用戶生成的密碼密文也不相同,這能夠極大的提升密碼的安全性。

傳統的加鹽方式須要在數據庫中有專門的字段來記錄鹽值,這個字段多是用戶名字段(由於用戶名惟一),也多是一個專門記錄鹽值的字段,這樣的配置比較繁瑣。

Spring Security 提供了多種密碼加密方案,官方推薦使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 強哈希函數,開發者在使用時能夠選擇提供 strength 和 SecureRandom 實例。strength 越大,密鑰的迭代次數越多,密鑰迭代次數爲 2^strength。strength 取值在 4~31 之間,默認爲 10。

不一樣於 Shiro 中須要本身處理密碼加鹽,在 Spring Security 中,BCryptPasswordEncoder 就自帶了鹽,處理起來很是方便。

3.實踐

3.1 codec 加密

commons-codec 是一個 Apache 上的開源項目,用它能夠方便的實現密碼加密。鬆哥在 V 部落 項目中就是採用的這種方案(https://github.com/lenve/VBlog)。在 Spring Security 還未推出 BCryptPasswordEncoder 的時候,commons-codec 仍是一個比較常見的解決方案。

因此,這裏我先來給你們介紹下 commons-codec 的用法。

首先咱們須要引入 commons-codec 的依賴:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

而後自定義一個 PasswordEncoder:

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));
    }
}

在 Spring Security 中,PasswordEncoder 專門用來處理密碼的加密與比對工做,咱們自定義 MyPasswordEncoder 並實現 PasswordEncoder 接口,還須要實現該接口中的兩個方法:

  1. encode 方法表示對密碼進行加密,參數 rawPassword 就是你傳入的明文密碼,返回的則是加密以後的密文,這裏的加密方案採用了 MD5。
  2. matches 方法表示對密碼進行比對,參數 rawPassword 至關因而用戶登陸時傳入的密碼,encodedPassword 則至關因而加密後的密碼(從數據庫中查詢而來)。

最後記得將 MyPasswordEncoder 經過 @Component 註解標記爲 Spring 容器中的一個組件。

這樣用戶在登陸時,就會自動調用 matches 方法進行密碼比對。

固然,使用了 MyPasswordEncoder 以後,在用戶註冊時,就須要將密碼加密以後存入數據庫中,方式以下:

public int reg(User user) {
    ...
    //插入用戶,插入以前先對密碼進行加密
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    result = userMapper.reg(user);
    ...
}

其實很簡單,就是調用 encode 方法對密碼進行加密。完整代碼你們能夠參考 V 部落(https://github.com/lenve/VBlog),我這裏就不贅述了。

3.2 BCryptPasswordEncoder 加密

可是本身定義 PasswordEncoder 仍是有些麻煩,特別是處理密碼加鹽問題的時候。

因此在 Spring Security 中提供了 BCryptPasswordEncoder,使得密碼加密加鹽變得很是容易。只須要提供 BCryptPasswordEncoder 這個 Bean 的實例便可,微人事就是採用了這種方案(https://github.com/lenve/vhr),以下:

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);
}

建立 BCryptPasswordEncoder 時傳入的參數 10 就是 strength,即密鑰的迭代次數(也能夠不配置,默認爲 10)。同時,配置的內存用戶的密碼也再也不是 123 了,以下:

auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("ADMIN", "USER")
.and()
.withUser("sang")
.password("$2a$10$eUHbAOMq4bpxTvOVz33LIehLe3fu6NwqC9tdOcxJXEhyZ4simqXTC")
.roles("USER");

這裏的密碼就是使用 BCryptPasswordEncoder 加密後的密碼,雖然 admin 和 sang 加密後的密碼不同,可是明文都是 123。配置完成後,使用 admin/123 或者 sang/123 就能夠實現登陸。

本案例使用了配置在內存中的用戶,通常狀況下,用戶信息是存儲在數據庫中的,所以須要在用戶註冊時對密碼進行加密處理,以下:

@Service
public class RegService {
    public int reg(String username, String password) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
        String encodePasswod = encoder.encode(password);
        return saveToDb(username, encodePasswod);
    }
}

用戶將密碼從前端傳來以後,經過調用 BCryptPasswordEncoder 實例中的 encode 方法對密碼進行加密處理,加密完成後將密文存入數據庫。

4.源碼淺析

最後咱們再來稍微看一下 PasswordEncoder。

PasswordEncoder 是一個接口,裏邊只有三個方法:

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
  • encode 方法用來對密碼進行加密。
  • matches 方法用來對密碼進行比對。
  • upgradeEncoding 表示是否須要對密碼進行再次加密以使得密碼更加安全,默認爲 false。

Spring Security 爲 PasswordEncoder 提供了不少實現:

可是老實說,自從有了 BCryptPasswordEncoder,咱們不多關注其餘實現類了。

PasswordEncoder 中的 encode 方法,是咱們在用戶註冊的時候手動調用。

matches 方法,則是由系統調用,默認是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中調用的。

protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
    String presentedPassword = authentication.getCredentials().toString();
    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");
        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
}

能夠看到,密碼比對就是經過 passwordEncoder.matches 方法來進行的。

關於 DaoAuthenticationProvider 的調用流程,你們能夠參考 SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)一文。

好了,今天就和小夥伴們簡單聊一聊 Spring Security 加密問題,小夥伴們要是有收穫記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索