基本 Spring security 快速入門
基本 Spring security 快速入門
最近學了 spring security 遇到一些 6 版和舊版的一些差異,所以想說寫一下快速入門,但有些前情提要:
- 這個入門不包含 spring security 的使用概念,關於這部分相當推薦閱讀 spring security 實戰這本書
- 本快速入門不包含 OAuth2 和權限控管,只會使用 Basic 驗證,但建議先學會入門之後再加深.
- 由於個人專案有使用到 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
之後會發現我們需要實作 authenticate
和 supports
接下來就依照順序說明.
實作 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 驗證
留言
張貼留言