身份驗證是保護 Spring Boot 應用程式安全的一個關鍵方面。在某些專案中,您可能會遇到需要為應用程式的不同部分支援多種身份驗證方法。
在我正在進行的 Spring Boot 副專案中,我遇到了一個與使用各種方法驗證 API 相關的令人著迷且常見的挑戰。具體來說,在處理以 /api/internal 為字首的內部 API 時,我選擇透過標頭中嵌入的 API 金鑰進行使用者身份驗證。相反,對於 Web 應用程式使用者介面,首選身份驗證方法是 HttpBasic。事實證明,在單個專案中管理多個身份驗證機制是一個值得注意的方面。
在本次討論中,我將提供一個說明性示例來說明如何實現這些不同的身份驗證方法。
1. 專案設定
確保您的build.gradle(對於 Gradle)或pom.xml(對於 Maven)檔案中有必要的依賴項:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
|
2.安全配置
建立一個 SecurityConfig 類來配置 Spring Security。你可以為應用程式的不同部分建立多個 SecurityConfigurerAdapter 類。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity @Configuration public class SpringSecurityConfig { @Autowired public APIAuthenticationErrEntrypoint apiAuthenticationErrEntrypoint; @Value(<font>"${internal.api-key}") private String internalApiKey; @Bean @Order(1) public SecurityFilterChain filterChainPrivate(HttpSecurity http) throws Exception { http .securityMatcher("/api/internal/**") .addFilterBefore(new InternalApiKeyAuthenticationFilter(internalApiKey), ChannelProcessingFilter.class) .exceptionHandling((auth) -> { auth.authenticationEntryPoint(apiAuthenticationErrEntrypoint); }) .cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable); return http.build(); } @Bean @Order(2) public SecurityFilterChain filterChainWebAppication(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/login").permitAll() .requestMatchers("/**").authenticated() .anyRequest().authenticated() ); http.formLogin(authz -> authz .loginPage("/login").permitAll() .loginProcessingUrl("/login") ); http.logout(authz -> authz .deleteCookies("JSESSIONID") .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) ); http.csrf(AbstractHttpConfigurer::disable); return http.build(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); return authenticationProvider; } @Bean public InMemoryUserDetailsManager userDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } }
|
3.實施身份驗證過濾器
為 API 金鑰身份驗證建立自定義過濾器:
public class InternalApiKeyAuthenticationFilter implements Filter {
private final String internalApiKey; InternalApiKeyAuthenticationFilter(String internalApiKey) { this.internalApiKey = internalApiKey; } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; String apiKey = httpServletRequest.getHeader(<font>"x-api-key"); if (apiKey == null) { unauthorized(httpServletResponse); return; } if (!internalApiKey.equals(apiKey)) { unauthorized(httpServletResponse); return; } chain.doFilter(request, response); } @Override public void destroy() { } private void unauthorized(HttpServletResponse httpServletResponse) throws IOException { httpServletResponse.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); httpServletResponse.setStatus(401); Map<String, Object> response = Map.of("message", "SC_UNAUTHORIZED"); String responseBody = new ObjectMapper().writeValueAsString(response); httpServletResponse.getWriter().write(responseBody); } }
|
一些使用 /api/internal/** 模式的請求將透過 InternalApiKeyAuthenticationFilter 從請求頭獲取 API 金鑰,並與常量金鑰進行比較以進行驗證。
4.在控制器中使用
在控制器中,使用 @PreAuthorize 註解為不同的端點指定所需的角色。
@RestController @RequestMapping(value = <font>"/api/internal") public class InternalAPIController {
@GetMapping(value = "/health") public ResponseEntity internalHealthCheck() { return ResponseEntity.ok("ok"); } } @Controller @RequestMapping(value = "/") public class HomeController { @GetMapping public String homePage(Model model) { return "home"; } }
|
結論
在 Spring Boot 專案中實施多種身份驗證方法,可以滿足應用程式不同部分的特定安全要求。透過利用 Spring Security 和自定義過濾器,您可以在同一專案中無縫整合 API 金鑰身份驗證和 HTTP 基本身份驗證。
切記要自定義身份驗證過濾器,以適應您的特定用例,並根據您的安全要求實施驗證邏輯。這種靈活的身份驗證方法可確保您的應用程式保持安全,同時滿足不同的身份驗證需求。
https://github.com/jackynote/springboot3-multi-authenticate