《深入淺出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;
}
}
-
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 入門,表單配置還有很多細節,下篇文章我們繼續。
如果文章幫助到小夥伴了,記得右下角點個在看哦。
一起切磋Web安全
(已添加松哥微信的小夥伴請勿重複添加)
本文分享自微信公衆號 - 江南一點雨(a_javaboy)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。