Spring Security and JWT Authentication

Introduction

Public APIs need authentication (who are you?) and authorization (what may you do?). Spring Security filters every request; JWT tokens carry signed claims for stateless APIs. This chapter secures REST endpoints, implements login that returns JWT, and protects routes by role—foundation for production apps.

Prerequisites

Add Dependencies

Security Filter Chain (Boot 3)

Code explanation:

  • Stateless — no HTTP session; JWT carries identity
  • csrf disabled — common for pure JSON APIs (re-enable for cookie-based forms)

JWT Utility

application.yml:

yaml
app:
  jwt:
    secret: ${JWT_SECRET:change-me-to-long-random-string-at-least-32-chars}
    expiration-ms: 3600000

Warning

Strong JWT Secret in Production

Use long random JWT_SECRET from env/secrets manager—never commit real secret to Git.

JWT Filter

Login Endpoint

Provide UserDetailsService bean loading users from DB with encoded passwords.

Call Protected API

bash
curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"pass"}'
 
curl http://localhost:8080/api/users/1 \
  -H "Authorization: Bearer <token>"

Method Security (Optional)

java
@EnableMethodSecurity
@Configuration
public class MethodSecurityConfig { }
 
@Service
public class AdminService {
    @PreAuthorize("hasRole('ADMIN')")
    public void purgeCache() { }
}

CORS

java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:5173"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

Enable in HttpSecurity: .cors(Customizer.withDefaults()).

FAQ

401 on every request?

Missing/invalid Bearer token, wrong secret, expired JWT.

403 vs 401?

401 not authenticated; 403 authenticated but insufficient role.

Store JWT where?

Browser SPA: memory or httpOnly cookie—avoid localStorage if XSS risk high.

Refresh tokens?

Issue short access + long refresh token stored securely—separate endpoint pattern.

OAuth2 / Login with Google?

Use spring-boot-starter-oauth2-client—different flow from custom JWT.

Password in DB?

Always BCrypt or Argon2—never plain text.

SpringDoc + JWT?

Add security scheme in OpenAPI—chapter 20—Authorize button in Swagger UI.