Spring Boot Security: JWT Authentication Step-by-Step

boot.cloud
Boot to Cloud
Published on Apr, 20 2026 3 min read 0 comments
image

In Week 4, we connected our Spring Boot APIs to a real database using JPA and Hibernate.
Now, in Week 5, it’s time to secure your APIs with Spring Security and JWT (JSON Web Token) authentication — the standard for modern web and mobile apps.

By the end of this article, your APIs will be protected, supporting login, registration, and role-based access control.

Why JWT + Spring Security?

Traditional session-based authentication has challenges:

  • Hard to scale in microservices
  • Requires server-side session storage
  • Not mobile/web friendly

JWT solves these problems:

  • Stateless authentication (no server session)
  • Works across multiple services
  • Easy to integrate with mobile and SPA frontends

Step 1: Add Dependencies

In your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

This includes Spring Security + JJWT library for token generation and validation.

Step 2: User Entity & Roles

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    private String role; // e.g., ROLE_USER, ROLE_ADMIN

    // Getters & Setters
}

Passwords must be hashed before storing. Never store plaintext passwords.

Step 3: Password Encryption

Use BCryptPasswordEncoder:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Save user with encoded password:

user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userRepository.save(user);

Step 4: JWT Utility Class

@Component
public class JwtUtil {
    private final String SECRET_KEY = "your_secret_key_here";

    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .claim("role", userDetails.getAuthorities().toString())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1 hour
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public String extractUsername(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY)
                   .parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY)
                   .parseClaimsJws(token).getBody().getExpiration().before(new Date());
    }
}

Step 5: UserDetails & Service

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                      .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                List.of(new SimpleGrantedAuthority(user.getRole()))
        );
    }
}

Step 6: Security Configuration

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtFilter jwtFilter;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
}
  • /api/auth/** → Public endpoints (login/register)
  • All other endpoints → Protected
  • Stateless sessions → JWT-based

Step 7: Authentication Controller

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody AuthRequest request) throws Exception {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
            );
        } catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password", e);
        }

        final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);
        return ResponseEntity.ok(jwt);
    }
}

AuthRequest DTO:

public class AuthRequest {
    private String username;
    private String password;
    // Getters & Setters
}

Step 8: Role-Based Access Example

@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
    return "Admin content";
}

@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public String userEndpoint() {
    return "User content";
}

Spring Security automatically restricts endpoints based on roles defined in your JWT.

Step 9: Testing Your Secured API

  1. Register userPOST /api/auth/register (optional)
  2. LoginPOST /api/auth/login → Receive JWT token
  3. Access protected endpoints → Add header:
Authorization: Bearer <JWT_TOKEN>

Week 5 Recap

✅ Spring Security setup
✅ JWT authentication flow
✅ Password hashing
✅ Role-based access control
✅ Securing REST APIs

Your backend is now production-ready for authentication.

What’s Next? (Week 6)

👉 Professional Error Handling & Logging in Spring Boot

We’ll cover:

  • Global exception handling
  • Custom exceptions
  • Logging with SLF4J & Logback
  • Production logging strategies

📌 Action Items

✔ Implement login & JWT authentication
✔ Protect all REST endpoints
✔ Test role-based access with Postman
✔ Push secured code to GitHub

0 Comments