Spring Boot 3中实现多种身份验证方法开源案例

身份验证是保护 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("${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("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 = "/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