Implementing Multiple Authentication Methods in a Spring Boot 3
Authentication is a critical aspect of securing your Spring Boot applications. In some projects, you might encounter the need to support multiple authentication methods for different parts of your application.
In my ongoing Spring Boot side project, I’ve encountered a fascinating and common challenge related to authenticating APIs using various methods. Specifically, when dealing with internal APIs prefixed with /api/internal, I opt for user authentication via API Key embedded in the header. Conversely, for web application user interfaces, the preferred authentication method is HttpBasic. Managing multiple authentication mechanisms within a single project has proven to be a noteworthy aspect.
In this discussion, I’ll provide an illustrative example of how to implement these diverse authentication approaches.
1. Project Setup
Ensure you have the necessary dependencies in your build.gradle
(for Gradle) or pom.xml
(for Maven) file:
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-security'
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. Security Configuration
Create a SecurityConfig
class to configure Spring Security. You can have multiple SecurityConfigurerAdapter
classes for different parts of your application.
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. Implement Authentication Filters
Create a custom filter for API Key authentication:
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);
}
}
Some requests with pattern /api/internal/**
will go through InternalApiKeyAuthenticationFilter
and get API key from request header and compare with constant key to verify.
4. Usage in Controller
In your controllers, use @PreAuthorize
annotation to specify the required roles for different endpoints.
@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";
}
}
Conclusion
Implementing multiple authentication methods in a Spring Boot project allows you to cater to different parts of your application with specific security requirements. By leveraging Spring Security and custom filters, you can seamlessly integrate API key authentication and HTTP basic authentication within the same project.
Remember to customize the authentication filter to suit your specific use case, and implement validation logic according to your security requirements. This flexible approach to authentication ensures that your application remains secure while accommodating diverse authentication needs.
Full code demo: https://github.com/jackynote/springboot3-multi-authenticate