Spring Security:初體驗

當我們在一個項目中引入 Spring Security 相關依賴後,默認的就是表單登錄,因此我們就從表單登錄開始講起。

新建項目

pom.xml引入依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

項目創建成功後,Spring Security 的依賴就添加進來了,在 Spring Boot 中我們加入的是 spring-boot-starter-security ,其實主要是這兩個:
在這裏插入圖片描述
我們添加一個測試的 HelloController

@RestController
public class HelloController {

	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}

}

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

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

Using generated security password: 196d59d7-6a16-45e5-a66e-b2e68a31129c

這就是 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。

用戶配置

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

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

配置文件方式

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

怎麼配置呢?默認的用戶就定義在SecurityProperties裏邊,是一個靜態內部類,我們如果要定義自己的用戶名密碼,必然是要去覆蓋默認配置,我們先來看下 SecurityProperties 的定義:

@ConfigurationProperties(prefix = "spring.security")
public class SecurityConfig {

}

接着我們只需要以 spring.security.user 爲前綴,去定義用戶名密碼即可:

spring.security.user.name=javakf
spring.security.user.password=123456

在 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 之後,控制檯就不會打印默認的密碼了。

此時重啓項目,就可以使用自己定義的用戶名/密碼登錄了。

配置類方式

除了上面的配置文件這種方式之外,我們也可以在配置類中配置用戶名/密碼。

在配置類中配置,我們就要指定 PasswordEncoder 了,這是一個非常關鍵的東西。

考慮到有的小夥伴對於 PasswordEncoder 還不太熟悉,因此,我這裏先稍微給大家介紹一下 PasswordEncoder 到底是幹嘛用的。要說 PasswordEncoder ,就得先說密碼加密。

爲什麼要加密

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

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

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

  • 等等不一而足,你會發現很多程序員的人文素養還是非常高的,讓人嘖嘖稱奇。

加密方案

密碼加密我們一般會用到散列函數,又稱散列算法、哈希函數,這是一種從任何數據中創建數字“指紋”的方法。散列函數把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定下來,然後將數據打亂混合,重新創建一個散列值。散列值通常用一個短的隨機字母和數字組成的字符串來代表。好的散列函數在輸入域中很少出現散列衝突。在散列表和數據處理中,不抑制衝突來區別數據,會使得數據庫記錄更難找到。我們常用的散列函數有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。

但是僅僅使用散列函數還不夠,爲了增加密碼的安全性,一般在密碼加密過程中還需要加鹽,所謂的鹽可以是一個隨機數也可以是用戶名,加鹽之後,即使密碼明文相同的用戶生成的密碼密文也不相同,這可以極大的提高密碼的安全性。但是傳統的加鹽方式需要在數據庫中有專門的字段來記錄鹽值,這個字段可能是用戶名字段(因爲用戶名唯一),也可能是一個專門記錄鹽值的字段,這樣的配置比較繁瑣。

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

不同於 Shiro 中需要自己處理密碼加鹽,在 Spring Security 中,BCryptPasswordEncoder 就自帶了鹽,處理起來非常方便。

而 BCryptPasswordEncoder 就是 PasswordEncoder 接口的實現類。

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 是否還要進行再次加密,這個一般來說就不用了。

配置

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

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("test")
                .password("123456").roles("admin");
    }
}
  1. 首先我們自定義 SecurityConfig 繼承自 WebSecurityConfigurerAdapter,重寫裏邊的
    configure 方法。
  2. 首先我們提供了一個 PasswordEncoder 的實例,因爲目前的案例還比較簡單,因此我暫時先不給密碼進行加密,所以返回 NoOpPasswordEncoder 的實例即可。
  3. configure 方法中,我們通過 inMemoryAuthentication 來開啓在內存中定義用戶,withUser
    中是用戶名,password 中則是用戶密碼,roles 中是用戶角色。
  4. 如果需要配置多個用戶,用 and 相連。

配置完成後,再次啓動項目,Java 代碼中的配置會覆蓋掉配置文件中的配置,此時再去訪問 /hello 接口,就會發現只有 Java 代碼中的用戶名/密碼才能訪問成功。

自定義表單登錄頁

默認的表單登錄有點醜(實際上現在默認的表單登錄比以前的好多了,以前的更醜)。

但是很多時候我們依然絕對這個登錄頁面有點醜,那我們可以自定義一個登錄頁面。

服務端定義

然後接下來我們繼續完善前面的 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 ,裏邊會有一個重要的標籤 ,HttpSecurity
    提供的配置方法 都對應了該標籤。
  3. authorizeRequests 對應了 。
  4. formLogin 對應了 。
  5. and 方法表示結束當前標籤,上下文回到HttpSecurity,開啓新一輪的配置。
  6. permitAll 表示登錄相關的頁面/接口不要被攔截。
  7. 最後記得關閉 csrf ,關於 csrf 問題我到後面專門和大家說。

當我們定義了登錄頁面爲 /login.html 的時候,Spring Security 也會幫我們自動註冊一個 /login.html 的接口,這個接口是 POST 請求,用來處理登錄邏輯。

前端定義

<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 ,其他的都是常規操作,我就不重複介紹了。

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

代碼託管:springsecurity_example_1

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