Spring Boot: Security with HTTP Basic & JWT
Spring Security is the standard security framework for Spring Boot applications. It provides authentication (who are you?) and authorization (what can you do?) as a chain of servlet filters. This article covers HTTP Basic auth, custom UserDetailsService, and stateless JWT-based authentication.
Contents
Adding Spring Security
Add the snippet below to your project. No further bootstrapping is needed: Spring Boot's auto-configuration picks it up from the classpath.
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT for JWT support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
Default Security Behaviour
Adding the starter immediately secures all endpoints. Spring Boot auto-generates a password (printed in the console) and registers a default user named user
- All HTTP endpoints require authentication.
- A login form is provided at
./login - The auto-generated password changes on every restart.
- Override
andspring.security.user.name inspring.security.user.password for a fixed test user.application.properties
HTTP Basic Auth Config
A minimal configuration that requires authentication for every request and uses HTTP Basic.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()); // enable HTTP Basic authentication
return http.build();
}
}
Custom UserDetailsService
Implement UserDetailsService
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPasswordHash()) // must be BCrypt encoded
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
// Register the PasswordEncoder bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Encode password before saving
String encoded = passwordEncoder.encode("rawPassword");
JWT: Generating a Token
This service creates a signed JWT for an authenticated user.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
@Service
public class JwtService {
private static final String SECRET = "my-very-secret-key-256-bit-minimum-length!";
private static final long EXPIRY_MS = 86_400_000L; // 24 hours
private Key signingKey() {
return Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(String username, List<String> roles) {
return Jwts.builder()
.subject(username)
.claim("roles", roles)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRY_MS))
.signWith(signingKey())
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.verifyWith((SecretKey) signingKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isExpired(token);
}
private boolean isExpired(String token) {
return Jwts.parser().verifyWith((SecretKey) signingKey()).build()
.parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
}
JWT: Validating with a Filter
The filter reads the token from the Authorization
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.security.authentication.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final CustomUserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService, CustomUserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
SecurityFilterChain Configuration
The filter chain makes the API stateless and runs the JWT filter before the standard authentication filter.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(JwtAuthFilter jwtAuthFilter,
CustomUserDetailsService userDetailsService) {
this.jwtAuthFilter = jwtAuthFilter;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // stateless API — disable CSRF
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // login endpoint
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// Login endpoint — authenticates and returns a JWT
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtService jwtService;
private final CustomUserDetailsService userDetailsService;
// constructor...
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
UserDetails user = userDetailsService.loadUserByUsername(request.getUsername());
List<String> roles = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList();
String token = jwtService.generateToken(request.getUsername(), roles);
return ResponseEntity.ok(Map.of("token", token));
}
}