使用 Spring Security


之前使用過的 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 {
}

這並不難理解,類似〈重構控制器〉中談到的 AbstractAnnotationConfigDispatcherServletInitializerAbstractSecurityWebApplicationInitializer 也是 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[] {"/"};
    }
}

現在可以啟動應用程式,一開始會被重新導向至自動產生的登入頁面,輸入以上設定的名稱與密碼,就可以完成登入:

使用 Spring Security

想要登出的話,可以直接請求 logout,會出現預設的登出確認畫面:

使用 Spring Security

按下 Log Out 之後,會回到原本的 login,並附上 logut 請求參數:

使用 Spring Security

如果登入失敗,預設會重新導向回 login,並附上 error 請求參數:

使用 Spring Security

你可以在 SpringSecurity 找到以上的範例專案。