手把手帶你入門 Spring Security,別再問密碼怎麼解密了

1.新建項目

首先新建一個 Spring Boot 項目,創建時引入 Spring Security 依賴和 web 依賴,如下圖:

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


手把手帶你入門 Spring Security,別再問密碼怎麼解密了


項目創建成功後,我們添加一個測試的 HelloController,內容如下:

<code>@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
複製代碼/<code>

接下來什麼事情都不用做,我們直接來啟動項目。

在項目啟動過程中,我們會看到如下一行日誌:

<code>Using generated security password: 30abfb1f-36e1-446a-a79b-f70024f589ab
複製代碼/<code>

這就是 Spring Security 為默認用戶 user 生成的臨時密碼,是一個 UUID 字符串。

接下來我們去訪問 http://localhost:8080/hello 接口,就可以看到自動重定向到登錄頁面了:


手把手帶你入門 Spring Security,別再問密碼怎麼解密了


在登錄頁面,默認的用戶名就是 user,默認的登錄密碼則是項目啟動時控制檯打印出來的密碼,輸入用戶名密碼之後,就登錄成功了,登錄成功後,我們就可以訪問到 /hello 接口了。

在 Spring Security 中,默認的登錄頁面和登錄接口,都是 /login ,只不過一個是 get 請求(登錄頁面),另一個是 post 請求(登錄接口)。

大家可以看到,非常方便,一個依賴就保護了所有接口。

有人說,你怎麼知道知道生成的默認密碼是一個 UUID 呢?

這個其實很好判斷。

和用戶相關的自動化配置類在 UserDetailsServiceAutoConfiguration 裡邊,在該類的 getOrDeducePassword 方法中,我們看到如下一行日誌:

<code>if (user.isPasswordGenerated()) {
\tlogger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
複製代碼/<code>

毫無疑問,我們在控制檯看到的日誌就是從這裡打印出來的。打印的條件是 isPasswordGenerated 方法返回 true,即密碼是默認生成的。

進而我們發現,user.getPassword 出現在 SecurityProperties 中,在 SecurityProperties 中我們看到如下定義:

<code>/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
private boolean passwordGenerated = true;
複製代碼/<code>

可以看到,默認的用戶名就是 user,默認的密碼則是 UUID,而默認情況下,passwordGenerated 也為 true。

2.用戶配置

默認的密碼有一個問題就是每次重啟項目都會變,這很不方便。

在正式介紹數據庫連接之前,松哥先和大家介紹兩種非主流的用戶名/密碼配置方案。

2.1 配置文件

我們可以在 application.properties 中配置默認的用戶名密碼。

怎麼配置呢?大家還記得上一小節我們說的 SecurityProperties,默認的用戶就定義在它裡邊,是一個靜態內部類,我們如果要定義自己的用戶名密碼,必然是要去覆蓋默認配置,我們先來看下 SecurityProperties 的定義:

<code>@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
複製代碼/<code>

這就很清晰了,我們只需要以 spring.security.user 為前綴,去定義用戶名密碼即可:

<code>spring.security.user.name=javaboy
spring.security.user.password=123
複製代碼/<code>

這就是我們新定義的用戶名密碼。

在 properties 中定義的用戶名密碼最終是通過 set 方法注入到屬性中去的,這裡我們順便來看下 SecurityProperties.User#setPassword 方法:

<code>public void setPassword(String password) {
\tif (!StringUtils.hasLength(password)) {
\t\treturn;
\t}
\tthis.passwordGenerated = false;
\tthis.password = password;
}
複製代碼/<code>

從這裡我們可以看到,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 這個接口中就定義了三個方法:

<code>public interface PasswordEncoder {
\tString encode(CharSequence rawPassword);
\tboolean matches(CharSequence rawPassword, String encodedPassword);
\tdefault boolean upgradeEncoding(String encodedPassword) {
\t\treturn false;
\t}
}
複製代碼/<code>
  1. encode 方法用來對明文密碼進行加密,返回加密之後的密文。
  2. matches 方法是一個密碼校對方法,在用戶登錄的時候,將用戶傳來的明文密碼和數據庫中保存的密文密碼作為參數,傳入到這個方法中去,根據返回的 Boolean 值判斷用戶密碼是否輸入正確。
  3. upgradeEncoding 是否還要進行再次加密,這個一般來說就不用了。

通過下圖我們可以看到 PasswordEncoder 的實現類:

手把手帶你入門 Spring Security,別再問密碼怎麼解密了

2.2.4 配置

預備知識講完後,接下來我們來看具體如何配置:

<code>@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");
}
}
複製代碼/<code>
  1. 首先我們自定義 SecurityConfig 繼承自 WebSecurityConfigurerAdapter,重寫裡邊的 configure 方法。
  2. 首先我們提供了一個 PasswordEncoder 的實例,因為目前的案例還比較簡單,因此我暫時先不給密碼進行加密,所以返回 NoOpPasswordEncoder 的實例即可。
  3. configure 方法中,我們通過 inMemoryAuthentication 來開啟在內存中定義用戶,withUser 中是用戶名,password 中則是用戶密碼,roles 中是用戶角色。
  4. 如果需要配置多個用戶,用 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) 方法,如下:

<code>@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();
}/<code>
  1. web.ignoring() 用來配置忽略掉的 URL 地址,一般對於靜態文件,我們可以採用此操作。
  2. 如果我們使用 XML 來配置 Spring Security ,裡邊會有一個重要的標籤 <http>,HttpSecurity 提供的配置方法 都對應了該標籤。/<http>
  3. authorizeRequests 對應了 <intercept-url>。/<intercept-url>
  4. formLogin 對應了 /<code>

    form 表單中,注意 action 為 /login.html ,其他的都是常規操作,我就不重複介紹了。

    好了,配置完成後,再去重啟項目,此時訪問任意頁面,就會自動重定向到我們定義的這個頁面上來,輸入用戶名密碼就可以重新登錄了。


    作者:江南一點雨
    鏈接:https://juejin.im/post/5e7aa3dc6fb9a07c89152ff3


    分享到:


相關文章: