之前使用過的 gossip 專案,使用的是 Web 容器提供的安全管理機制,如果你曾經使用過這方面的安全管理機制,對於認識 Spring Security 是有幫助的,特別是在基於角色的存取控制(Role-based access control)這部份的觀念。
若你有興趣認識 Web 容器提供的安全管理機制,可以參考〈語言技術:Servlet/JSP〉中的相關文件,如果對於 gossip 專案中,怎麼使用 Web 容器提供的安全管理機制,可以參考《Servlet & JSP技術手冊 - 從 Servlet 到 Spring Boot》。
既然先前都在重構 gossip 專案,那麼自然地,下一步就會想到,能不能用 Spring Security 來取代 Web 容器提供的安全管理機制呢?至少馬上可以想像到的是,若能基於 JavaConfig 的方式進行相關安全設定,會不會比較有彈性呢?
實際上確實是有的,而且有個重構 gossip 專案套用 Spring Security 的目標,對於接觸 Spring Security 是有幫助的,畢竟 Spring Security 也是有著非常多的功能與元件,從先解決身邊的需求開始,比較不會迷失在一堆的功能與設定之中。
為了有個簡單的開始,我準備了一個 SpringSecurity 專案,在部署之後,可以請求任一路徑,應用程式只是很簡單地顯示你請求了哪個路徑而已,技術上來說,該專案有基本的 Spring MVC 設定,像是使用 Thymeleaf 模版,有個簡單的控制器抽取路徑資訊,沒有任何的路徑防護,因而接下來,就可以在這個專案上,專心於認識 Spring Security 的相關設定與元件。
就目前來說,為了要能使用 Spring Security,相依的程式庫可以在 build.gradle 中加入管理:
compile 'org.springframework.security:spring-security-core:5.1.2.RELEASE'
compile 'org.springframework.security:spring-security-config:5.1.2.RELEASE'
compile 'org.springframework.security:spring-security-web:5.1.2.RELEASE'
想在 Web 容器中使用 Spring Security 的功能,技術上來說是透過過濾器實作,具體來說,必須有個 org.springframework.web.filter.DelegatingFilterProxy
過濾器,可設定 <url-pattern>
為 /*
,然而實際上,可以繼承 AbstractSecurityWebApplicationInitializer
package cc.openhome.web;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
}
這並不難理解,類似〈重構控制器〉中談到的 AbstractAnnotationConfigDispatcherServletInitializer
,AbstractSecurityWebApplicationInitializer
也是 WebApplicationInitializer
的實作類別之一,會在應用程式初始化時進行 DelegatingFilterProxy
過濾器的建立與設定。
接著來建立一個 SecurityConfig
類別,作為安全設定時組態檔案,別忘了要加上 @Configuration
與 @EnableWebSecurity
:
package cc.openhome.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder pwdEncoder = new BCryptPasswordEncoder();
auth.inMemoryAuthentication() // 驗證資訊存放於記憶體
.passwordEncoder(pwdEncoder)
.withUser("admin")
.password(pwdEncoder.encode("admin12345678"))
.roles("ADMIN", "MEMBER")
.and()
.withUser("caterpillar")
.password(pwdEncoder.encode("12345678"))
.roles("MEMBER");
}
}
為了有個簡單的開始,在這邊先將驗證時的相關資訊存放在記憶體中,而使用者的密碼不要存明碼,現在已經是個基本安全認知了,因而從 Spring Security 5 開始,強制必須對密碼進行編碼,具體而言,方式之一就是直接指定 PasswordEncoder
,在這邊使用的是 BCryptPasswordEncoder
,並透過 encode
對密碼編碼,實際上這會是在使用者註冊時進行這動作,不過這邊先暫且寫在設定檔裡。
BCryptPasswordEncoder
實作了 bcrypt 加密演算法,是 Spring 官方推薦的加密方式,它會使用一個加鹽的流程以防禦彩虹表攻擊,就算是相同的密碼,因為每次產生的鹽值不同,編碼後的結果也就不會相同(鹽值會包含在編碼後的結果之中,不過 bcrypt 屬於 Slow Hash Function 手法,也就是破解它的時間成本高,高到可以讓攻擊者放棄)。
若不指定使用的 PasswordEncoder
,在儲存密碼時,必須在加密過的密碼前加上加密算法前置 "{id}"
,例如:
package cc.openhome.web;
...
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("{bcrypt}$2a$10$PUFa4u8d434aWitf87scE.vue580tghpCU6JdPnDXQgjK1q0Ddtgu")
.roles("ADMIN", "MEMBER")
.and()
.withUser("caterpillar")
.password("{bcrypt}$2a$10$yh5WJetawp2KloUtEoVzRuT4/WEeR5BhPdfRZGoAvnCtKAbFBP8Sa")
.roles("MEMBER");
}
}
你應該花點時間來瞭解一下密碼加密算法,以及 Spring Security 處理密碼的方式,這部份可以參考〈Password Encoding〉。
由於 Web 應用程式的開發人員在進行授權管理時,無法事先得知這個應用程式將部署在哪個伺服器上,也就無法直接使用伺服器系統上的使用者及群組來進行授權管理,而必須根據角色來定義,屆時 Web 應用程式真正部署至伺服器時,再透過伺服器特定的設定方式,將角色對應至使用者或群組。
在這邊建立使用者名稱、密碼與設定角色,就類似於伺服器上將角色對應至使用者的動作,之後想要設定某些資源必須防護,就可以只設定該資源可被哪些角色給存取。
目前沒有設定任何要防護的頁面路徑以及登入資訊,預設就是防護全部的路徑請求,登入路徑會是 login
,登出路徑會是 logout
,並自動產生登入與登出頁面。
接著,這個設定檔必須加入 RootConfig 之中:
package cc.openhome.web;
import org.springframework.web.servlet.support.*;
public class MVCInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebConfig.class};
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {SecurityConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
現在可以啟動應用程式,一開始會被重新導向至自動產生的登入頁面,輸入以上設定的名稱與密碼,就可以完成登入:
想要登出的話,可以直接請求 logout
,會出現預設的登出確認畫面:
按下 Log Out 之後,會回到原本的 login
,並附上 logut
請求參數:
如果登入失敗,預設會重新導向回 login
,並附上 error
請求參數:
你可以在 SpringSecurity 找到以上的範例專案。