【SpringSecurity系列(二)】Spring Security入門

《深入淺出Spring Security》一書已由清華大學出版社正式出版發行,感興趣的小夥伴戳這裏->->>深入淺出Spring Security,一本書學會 Spring Security。

爲之前有小夥伴在松哥羣裏討論如何給微人事的密碼解密,我看到聊天記錄後就驚呆了。

無論如何我也得寫一篇文章,帶大家入門 Spring Security!當我們在一個項目中引入 Spring Security 相關依賴後,默認的就是表單登錄,因此我們就從表單登錄開始講起。

Spring Security 初體驗

通過 Java 類定義用戶

自定義登錄表單

視頻看完了,如果小夥伴們覺得松哥的視頻風格還能接受,也可以看看松哥自制的 Spring Boot + Vue 系列視頻教程

以下是視頻筆記。

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;
 }
}
  1. encode 方法用來對明文密碼進行加密,返回加密之後的密文。
  2. matches 方法是一個密碼校對方法,在用戶登錄的時候,將用戶傳來的明文密碼和數據庫中保存的密文密碼作爲參數,傳入到這個方法中去,根據返回的 Boolean 值判斷用戶密碼是否輸入正確。
  3. 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");
    }
}
  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) 方法,如下:

@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();
}
  1. web.ignoring() 用來配置忽略掉的 URL 地址,一般對於靜態文件,我們可以採用此操作。
  2. 如果我們使用 XML 來配置 Spring Security ,裏邊會有一個重要的標籤 <http>,HttpSecurity 提供的配置方法 都對應了該標籤。
  3. authorizeRequests 對應了 <intercept-url>
  4. formLogin 對應了 <formlogin>
  5. and 方法表示結束當前標籤,上下文回到HttpSecurity,開啓新一輪的配置。
  6. permitAll 表示登錄相關的頁面/接口不要被攔截。
  7. 最後記得關閉 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 入門,表單配置還有很多細節,下篇文章我們繼續。

如果文章幫助到小夥伴了,記得右下角點個在看哦。








加微信進羣



一起切磋Web安全

(已添加松哥微信的小夥伴請勿重複添加)

本文分享自微信公衆號 - 江南一點雨(a_javaboy)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章