Spring Boot JWT Authentication: Complete Security Guide
Implement JWT authentication in Spring Boot 3. Learn Spring Security 6, token generation, validation, refresh tokens, and role-based access control.
Moshiour Rahman
Advertisement
What is JWT Authentication?
JSON Web Token (JWT) is a compact, URL-safe token format for securely transmitting information between parties. It’s ideal for stateless REST API authentication.
JWT Structure
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNjE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| Part | Content |
|---|---|
| Header | Algorithm & token type |
| Payload | User data (claims) |
| Signature | Verification hash |
Project Setup
Dependencies (pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Application Properties
# application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
jwt:
secret: your-256-bit-secret-key-should-be-at-least-32-characters
expiration: 86400000 # 24 hours in milliseconds
refresh-expiration: 604800000 # 7 days
User Entity and Repository
User Entity
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
private String firstName;
private String lastName;
@Enumerated(EnumType.STRING)
private Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Role Enum
public enum Role {
USER,
ADMIN
}
User Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
JWT Service
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long jwtExpiration;
@Value("${jwt.refresh-expiration}")
private long refreshExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, refreshExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey())
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSignInKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JWT Authentication Filter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
// Check if Authorization header exists and starts with "Bearer "
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Extract JWT token
jwt = authHeader.substring(7);
try {
userEmail = jwtService.extractUsername(jwt);
// If user email exists and no authentication in context
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
// Validate token
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token has expired");
return;
} catch (JwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid token");
return;
}
filterChain.doFilter(request, response);
}
}
Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Authentication Service
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
public AuthenticationResponse register(RegisterRequest request) {
// Check if user already exists
if (userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("Email already registered");
}
var user = User.builder()
.firstName(request.getFirstName())
.lastName(request.getLastName())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER)
.build();
userRepository.save(user);
var jwtToken = jwtService.generateToken(user);
var refreshToken = jwtService.generateRefreshToken(user);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
}
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new RuntimeException("User not found"));
var jwtToken = jwtService.generateToken(user);
var refreshToken = jwtService.generateRefreshToken(user);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
}
public AuthenticationResponse refreshToken(String refreshToken) {
final String userEmail = jwtService.extractUsername(refreshToken);
if (userEmail != null) {
var user = userRepository.findByEmail(userEmail)
.orElseThrow(() -> new RuntimeException("User not found"));
if (jwtService.isTokenValid(refreshToken, user)) {
var accessToken = jwtService.generateToken(user);
return AuthenticationResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
throw new RuntimeException("Invalid refresh token");
}
}
DTOs
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String firstName;
private String lastName;
@Email
private String email;
@Size(min = 6)
private String password;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
private String email;
private String password;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
private String accessToken;
private String refreshToken;
}
Authentication Controller
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authService;
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@Valid @RequestBody RegisterRequest request
) {
return ResponseEntity.ok(authService.register(request));
}
@PostMapping("/login")
public ResponseEntity<AuthenticationResponse> authenticate(
@RequestBody AuthenticationRequest request
) {
return ResponseEntity.ok(authService.authenticate(request));
}
@PostMapping("/refresh-token")
public ResponseEntity<AuthenticationResponse> refreshToken(
@RequestHeader("Authorization") String authHeader
) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("Invalid refresh token");
}
String refreshToken = authHeader.substring(7);
return ResponseEntity.ok(authService.refreshToken(refreshToken));
}
}
Protected Controller Example
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class UserController {
@GetMapping("/user/profile")
public ResponseEntity<String> getUserProfile() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok("Hello, " + auth.getName());
}
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<String> getAdminData() {
return ResponseEntity.ok("Admin only data");
}
}
Testing the API
Register User
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"password": "password123"
}'
Login
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "password123"
}'
Access Protected Resource
curl http://localhost:8080/api/user/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Summary
| Component | Purpose |
|---|---|
| JwtService | Generate and validate tokens |
| JwtAuthenticationFilter | Intercept requests, validate JWT |
| SecurityConfig | Configure security rules |
| AuthenticationService | Handle login/register logic |
JWT authentication provides stateless security perfect for REST APIs and microservices.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Spring Security Architecture: Complete Guide to How Security Works
Master Spring Security internals. Learn FilterChain, SecurityContext, Authentication flow, and how Spring Security protects your application.
Spring BootSpring Security Database Authentication: Custom UserDetailsService Guide
Implement database authentication in Spring Security. Learn UserDetailsService, password encoding, account locking, and multi-tenant authentication.
Spring BootSpring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.