松哥手把手帶你入門 Spring Security,別再問密碼怎麼解密了
- 2020 年 3 月 27 日
- 筆記
因為之前有小夥伴在松哥群里討論如何給微人事的密碼解密,我看到聊天記錄後就驚呆了。
無論如何我也得寫一篇文章,帶大家入門 Spring Security!當我們在一個項目中引入 Spring Security 相關依賴後,默認的就是表單登錄,因此我們就從表單登錄開始講起。
「Spring Security 初體驗」
「通過 Java 類定義用戶」
「自定義登錄表單」
影片看完了,如果小夥伴們覺得松哥的影片風格還能接受,也可以看看松哥自製的影片
以下是影片筆記。
1.新建項目
首先新建一個 Spring Boot 項目,創建時引入 Spring Security 依賴和 web 依賴,如下圖:

項目創建成功後,Spring Security 的依賴就添加進來了,在 Spring Boot 中我們加入的是 spring-boot-starter-security
,其實主要是這兩個:

項目創建成功後,我們添加一個測試的 HelloController,內容如下:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } }
接下來什麼事情都不用做,我們直接來啟動項目。
在項目啟動過程中,我們會看到如下一行日誌:
Using generated security password: 30abfb1f-36e1-446a-a79b-f70024f589ab
這就是 Spring Security 為默認用戶 user 生成的臨時密碼,是一個 UUID 字元串。
接下來我們去訪問 http://localhost:8080/hello
介面,就可以看到自動重定向到登錄頁面了:

在登錄頁面,默認的用戶名就是 user,默認的登錄密碼則是項目啟動時控制台列印出來的密碼,輸入用戶名密碼之後,就登錄成功了,登錄成功後,我們就可以訪問到 /hello 介面了。
在 Spring Security 中,默認的登錄頁面和登錄介面,都是 /login
,只不過一個是 get 請求(登錄頁面),另一個是 post 請求(登錄介面)。
「大家可以看到,非常方便,一個依賴就保護了所有介面。」
有人說,你怎麼知道知道生成的默認密碼是一個 UUID 呢?
這個其實很好判斷。
和用戶相關的自動化配置類在 UserDetailsServiceAutoConfiguration
裡邊,在該類的 getOrDeducePassword
方法中,我們看到如下一行日誌:
if (user.isPasswordGenerated()) { logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword())); }
毫無疑問,我們在控制台看到的日誌就是從這裡列印出來的。列印的條件是 isPasswordGenerated 方法返回 true,即密碼是默認生成的。
進而我們發現,user.getPassword 出現在 SecurityProperties 中,在 SecurityProperties 中我們看到如下定義:
/** * Default user name. */ private String name = "user"; /** * Password for the default user name. */ private String password = UUID.randomUUID().toString(); private boolean passwordGenerated = true;
可以看到,默認的用戶名就是 user,默認的密碼則是 UUID,而默認情況下,passwordGenerated 也為 true。
2.用戶配置
默認的密碼有一個問題就是每次重啟項目都會變,這很不方便。
在正式介紹資料庫連接之前,松哥先和大家介紹兩種非主流的用戶名/密碼配置方案。
2.1 配置文件
我們可以在 application.properties 中配置默認的用戶名密碼。
怎麼配置呢?大家還記得上一小節我們說的 SecurityProperties,默認的用戶就定義在它裡邊,是一個靜態內部類,我們如果要定義自己的用戶名密碼,必然是要去覆蓋默認配置,我們先來看下 SecurityProperties 的定義:
@ConfigurationProperties(prefix = "spring.security") public class SecurityProperties {
這就很清晰了,我們只需要以 spring.security.user 為前綴,去定義用戶名密碼即可:
spring.security.user.name=javaboy spring.security.user.password=123
這就是我們新定義的用戶名密碼。
在 properties 中定義的用戶名密碼最終是通過 set 方法注入到屬性中去的,這裡我們順便來看下 SecurityProperties.User#setPassword 方法:
public void setPassword(String password) { if (!StringUtils.hasLength(password)) { return; } this.passwordGenerated = false; this.password = password; }
從這裡我們可以看到,application.properties 中定義的密碼在注入進來之後,還順便設置了 passwordGenerated 屬性為 false,這個屬性設置為 false 之後,控制台就不會列印默認的密碼了。
此時重啟項目,就可以使用自己定義的用戶名/密碼登錄了。
2.2 配置類
除了上面的配置文件這種方式之外,我們也可以在配置類中配置用戶名/密碼。
在配置類中配置,我們就要指定 PasswordEncoder 了,這是一個非常關鍵的東西。
考慮到有的小夥伴對於 PasswordEncoder 還不太熟悉,因此,我這裡先稍微給大家介紹一下 PasswordEncoder 到底是幹嘛用的。要說 PasswordEncoder ,就得先說密碼加密。
2.2.1 為什麼要加密
2011 年 12 月 21 日,有人在網路上公開了一個包含 600 萬個 CSDN 用戶資料的資料庫,數據全部為明文儲存,包含用戶名、密碼以及註冊郵箱。事件發生後 CSDN 在微博、官方網站等渠道發出了聲明,解釋說此資料庫系 2009 年備份所用,因不明原因泄露,已經向警方報案,後又在官網發出了公開道歉信。在接下來的十多天里,金山、網易、京東、噹噹、新浪等多家公司被捲入到這次事件中。整個事件中最觸目驚心的莫過於 CSDN 把用戶密碼明文存儲,由於很多用戶是多個網站共用一個密碼,因此一個網站密碼泄露就會造成很大的安全隱患。由於有了這麼多前車之鑒,我們現在做系統時,密碼都要加密處理。
這次泄密,也留下了一些有趣的事情,特別是對於廣大程式設計師設置密碼這一項。人們從 CSDN 泄密的文件中,發現了一些好玩的密碼,例如如下這些:
ppnn13%dkstFeb.1st
這段密碼的中文解析是:娉娉裊裊十三餘,豆蔻梢頭二月初。csbt34.ydhl12s
這段密碼的中文解析是:池上碧苔三四點,葉底黃鸝一兩聲- …
等等不一而足,你會發現很多程式設計師的人文素養還是非常高的,讓人嘖嘖稱奇。
2.2.2 加密方案
密碼加密我們一般會用到散列函數,又稱散列演算法、哈希函數,這是一種從任何數據中創建數字「指紋」的方法。散列函數把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定下來,然後將數據打亂混合,重新創建一個散列值。散列值通常用一個短的隨機字母和數字組成的字元串來代表。好的散列函數在輸入域中很少出現散列衝突。在散列表和數據處理中,不抑制衝突來區別數據,會使得資料庫記錄更難找到。我們常用的散列函數有 MD5 消息摘要演算法、安全散列演算法(Secure Hash Algorithm)。
但是僅僅使用散列函數還不夠,為了增加密碼的安全性,一般在密碼加密過程中還需要加鹽,所謂的鹽可以是一個隨機數也可以是用戶名,加鹽之後,即使密碼明文相同的用戶生成的密碼密文也不相同,這可以極大的提高密碼的安全性。但是傳統的加鹽方式需要在資料庫中有專門的欄位來記錄鹽值,這個欄位可能是用戶名欄位(因為用戶名唯一),也可能是一個專門記錄鹽值的欄位,這樣的配置比較繁瑣。
Spring Security 提供了多種密碼加密方案,官方推薦使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 強哈希函數,開發者在使用時可以選擇提供 strength 和 SecureRandom 實例。strength 越大,密鑰的迭代次數越多,密鑰迭代次數為 2^strength。strength 取值在 4~31 之間,默認為 10。
不同於 Shiro 中需要自己處理密碼加鹽,在 Spring Security 中,BCryptPasswordEncoder 就自帶了鹽,處理起來非常方便。
而 BCryptPasswordEncoder 就是 PasswordEncoder 介面的實現類。
2.2.3 PasswordEncoder
PasswordEncoder 這個介面中就定義了三個方法:
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
- encode 方法用來對明文密碼進行加密,返回加密之後的密文。
- matches 方法是一個密碼校對方法,在用戶登錄的時候,將用戶傳來的明文密碼和資料庫中保存的密文密碼作為參數,傳入到這個方法中去,根據返回的 Boolean 值判斷用戶密碼是否輸入正確。
- upgradeEncoding 是否還要進行再次加密,這個一般來說就不用了。
通過下圖我們可以看到 PasswordEncoder 的實現類:

2.2.4 配置
預備知識講完後,接下來我們來看具體如何配置:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("javaboy.org") .password("123").roles("admin"); } }
- 首先我們自定義 SecurityConfig 繼承自 WebSecurityConfigurerAdapter,重寫裡邊的 configure 方法。
- 首先我們提供了一個 PasswordEncoder 的實例,因為目前的案例還比較簡單,因此我暫時先不給密碼進行加密,所以返回 NoOpPasswordEncoder 的實例即可。
- configure 方法中,我們通過 inMemoryAuthentication 來開啟在記憶體中定義用戶,withUser 中是用戶名,password 中則是用戶密碼,roles 中是用戶角色。
- 如果需要配置多個用戶,用 and 相連。
為什麼用 and 相連呢?
❝在沒有 Spring Boot 的時候,我們都是 SSM 中使用 Spring Security,這種時候都是在 XML 文件中配置 Spring Security,既然是 XML 文件,標籤就有開始有結束,現在的 and 符號相當於就是 XML 標籤的結束符,表示結束當前標籤,這是個時候上下文會回到 inMemoryAuthentication 方法中,然後開啟新用戶的配置。 ❞
配置完成後,再次啟動項目,Java 程式碼中的配置會覆蓋掉 XML 文件中的配置,此時再去訪問 /hello 介面,就會發現只有 Java 程式碼中的用戶名/密碼才能訪問成功。
3.自定義表單登錄頁
默認的表單登錄有點丑(實際上現在默認的表單登錄比以前的好多了,以前的更丑)。
但是很多時候我們依然絕對這個登錄頁面有點丑,那我們可以自定義一個登錄頁面。
一起來看下。
3.1 服務端定義
然後接下來我們繼續完善前面的 SecurityConfig 類,繼續重寫它的 configure(WebSecurity web)
和 configure(HttpSecurity http)
方法,如下:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**","/images/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .permitAll() .and() .csrf().disable(); }
- web.ignoring() 用來配置忽略掉的 URL 地址,一般對於靜態文件,我們可以採用此操作。
- 如果我們使用 XML 來配置 Spring Security ,裡邊會有一個重要的標籤
<http>
,HttpSecurity 提供的配置方法 都對應了該標籤。 - authorizeRequests 對應了
<intercept-url>
。 - formLogin 對應了
<formlogin>
。 - and 方法表示結束當前標籤,上下文回到HttpSecurity,開啟新一輪的配置。
- permitAll 表示登錄相關的頁面/介面不要被攔截。
- 最後記得關閉 csrf ,關於 csrf 問題我到後面專門和大家說。
當我們定義了登錄頁面為 /login.html 的時候,Spring Security 也會幫我們自動註冊一個 /login.html 的介面,這個介面是 POST 請求,用來處理登錄邏輯。
3.2 前端定義
松哥這裡準備了一個還過得去的登錄頁面,如下:

我們將登錄頁面的相關靜態文件拷貝到 Spring Boot 項目的 resources/static 目錄下:

前端頁面比較長,這裡我把核心部分列出來(完整程式碼我會上傳到 GitHub:https://github.com/lenve/spring-security-samples):
<form action="/login.html" method="post"> <div class="input"> <label for="name">用戶名</label> <input type="text" name="username" id="name"> <span class="spin"></span> </div> <div class="input"> <label for="pass">密碼</label> <input type="password" name="password" id="pass"> <span class="spin"></span> </div> <div class="button login"> <button type="submit"> <span>登錄</span> <i class="fa fa-check"></i> </button> </div> </form>
form 表單中,注意 action 為 /login.html
,其他的都是常規操作,我就不重複介紹了。
好了,配置完成後,再去重啟項目,此時訪問任意頁面,就會自動重定向到我們定義的這個頁面上來,輸入用戶名密碼就可以重新登錄了。
4.小節
這篇文章和大家簡單聊一下 Spring Security 入門,表單配置還有很多細節,下篇文章我們繼續。