基本 Spring security 快速入門

基本 Spring security 快速入門

最近學了 spring security 遇到一些 6 版和舊版的一些差異,所以想說寫一下快速入門,但有些前情提要:

  1. 這個入門不包含 spring security 的使用概念,關於這部分相當推薦閱讀 spring security 實戰這本書
  2. 本快速入門不包含 OAuth2 和權限控管,只會使用 Basic 驗證,但建議先學會入門之後再加深.
  3. 由於個人專案有使用到 swagge ,因此也會有相對的教學

主要使用到的 spring security 元件:

  • org.springframework.security.authentication.AuthenticationProvider
  • org.springframework.security.crypto.password.PasswordEncoder
  • org.springframework.security.core.userdetails.UserDetailsService
  • org.springframework.security.core.userdetails.UserDetails

撰寫 UserDetailsService

首先先在 service 新增 class 並且 implements UserDetailsService
實作的時候會發現需要實作:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 

依照 function name 可以知道要實現的功能是去 DB 或 redis 之類的地方取得使用者資訊,並且將資料放進 UserDetails 後 return 出去,因此在開始寫 loadUserByUsername 方法之前需要先實作 UserDetails

實作 UserDetails

實作的時候會發現我們需要實作 getAuthorities getPassword getUsername isAccountNonExpired isAccountNonLocked isCredentialsNonExpired isEnabled 基本上依照 function name 和 java doc 可以理解用途.
由於我們只是要快速入門,因此我們暫時先將isAccountNonExpired isAccountNonLocked isCredentialsNonExpired isEnabled 寫死直接 return true;
接下來我們先新增 username 和 password 跟 grantedAuthorities 全域變數,並且建立一個建構函數,函數內容主要是 set 剛剛提到的username, password, grantedAuthorities ,完成之後就算是完成 UserDetails 了,詳細可以閱讀這邊

撰寫 UserDetailsService

新增 class 並且實作 UserDetailsService 由於這邊後續需要使用 AutoWrite ,因此我們幫他掛上 @Service ,之所以會需要使用 @Service ,是因為我們需要透過這個 service 去 dao 取得使用者資料.
接下來我們需要實作 loadUserByUsername 方法,實作方法相當簡單,就是透過 dao 取得資料之後透過我們剛剛寫的建構函數 return 出去,需要特別說明的是 UserDetails 的 grantedAuthorities ,由於我們目前沒有需要使用到權限控管,因此我們直接使用

List.of(new SimpleGrantedAuthority("admin")))

就可以了,想要看範例程式的話可以看這邊

撰寫 AuthenticationProvider

完成 UserDetailsService 之後可以撰寫 AuthenticationProvider
其實可以不撰寫 AuthenticationProvider ,但考慮到後續可能會有多個驗證方式,比如 SSO 或 OAuth 或我們這次的 Basic 驗證,因此為了後續擴充,我們在入門就先使用 AuthenticationProvider 在將來需要新增驗證手段時會更加方便.
實作 AuthenticationProvider 之後會發現我們需要實作 authenticatesupports 接下來就依照順序說明.

實作 authenticate

首先先新增 PasswordEncoder UserDetailsService 全域變數,並且 Autowired,接下來我們直接看程式碼來看要怎麼寫:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    try {
        var username = authentication.getName(); //取得 username
        var password = authentication.getCredentials().toString(); //由於這邊使用 Basic ,因此getCredentials()會取得密碼
        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new AuthenticationServiceException("Username or password is missing"); //如果為空就代表資料有問題,丟出錯誤
        }
        var userData = userDetailsService.loadUserByUsername(username); //去剛剛的 service 取得對應的 user 資料
        if (!passwordEncoder.matches(password, userData.getPassword())) { //透過 passwordEncoder 檢查密碼是否正確,為何要使用 passwordEncoder 後續會說明
            throw new AuthenticationServiceException("Invalid password"); //如果發現不同就拋錯出去
        }
        return new UsernamePasswordAuthenticationToken(username, password, userData.getAuthorities());
    } catch (Exception e) {
        log.error("authentication failed", e);
        throw new AuthenticationServiceException(e.getMessage());
    }
}

passwordEncoder 的用途補充說明

由於我們通常不會在資料庫存使用者原始密碼,通常會存無法還原成原始密碼的字串,因此我們需要透過 passwordEncoder 的 match 判斷 DB 密碼與取得的密碼是否相同.

support 方法

support 主要用於告知 spring security 這個 provider 是否用於當前 request 的驗證,那由於這個 provider 只有要用於驗證 Basic 而已,因此只要照下面的寫法就好了:

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

config

最後的最後,所有演員都到齊了,只需要在 spring boot 設定好相對應的 Bean 就可以使用了.
先新增一個 class 並掛上 @Configuration@EnableWebSecurity
接下來透過程式碼和註解說明如何使用.

/**
 * authorizeHttpRequests 在 http.authorizeRequests() 中使用,主要用於定義哪些 URL 需要被保護,以及它們需要的權限等。
 * 它是基於 HttpServletRequest 使用的。
 * 當使用 requestMatchers 方法時,你在該方法中定義的所有路徑都需要符合對應的安全要求或角色。
 * @param http
 * @param authenticationProvider
 * @return
 * @throws Exception
 */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationProvider authenticationProvider)
        throws Exception {
    var providerManager = new ProviderManager(Collections.singletonList(authenticationProvider));//建立 ProviderManager 將我們寫的 provider 放入
    return http
            .authorizeHttpRequests((requests) -> requests
                    .anyRequest().authenticated() //宣告所有 request 都需要驗證
            ).httpBasic(withDefaults()) //宣告使用 httpBasic
            .csrf(AbstractHttpConfigurer::disable) //禁用 csrf ,如果要走前後端分離的話可以考慮 disable
            .authenticationManager(providerManager) //放入 ProviderManager
            .build();
}

/**
 * 主要用於完全繞過 Spring Security 的所有過濾器鏈,對某些靜態資源如 CSS,JavaScript 檔案等使用。
 * 由於有用到 swagger ,因此關於 swagger 的部分需要放入,另外還有放入註冊帳號的 URI
 * @return
 */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.ignoring().requestMatchers("/js/**", "/css/**", "/accountController/account",
            "swagger-ui/**", "/swagger-ui.html", "open-api/**", "/v3/api-docs/**");
}

/**
 * 註冊用於 passwordEncoder 的 bean
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

最後的最後

由於我有需要在 swagger 使用驗證,因此需要多為 swagger 做設定,因此需要加入下列程式碼:

@SecurityScheme(
        type = SecuritySchemeType.HTTP,
        name = "basicAuth",
        scheme = "basic")

加入之後再到需要使用到的 controller 加上:@SecurityRequirement(name = "basicAuth"),此時打開 swagger 可以看到下方圖片的 Authorize:



只要在該位置輸入帳號密碼登入後下方的 request header 都會加入 Authorization 讓 spring security 驗證

留言

這個網誌中的熱門文章

記帳專案說明

Hana Project 介紹 第三集 - Hana 引擎後端架構(二)