Java Spring Security: JWT Authentication, Authorization and Best Practices
Spring Security is the standard security framework for Spring Boot applications. It is powerful but complex β developers often copy-paste configurations without truly understanding what they do. This guide explains the core concepts and walks through a real JWT authentication setup.
How Spring Security Works
Spring Security operates as a chain of filters that intercept every HTTP request before it reaches your controllers. You configure which URLs require authentication, which roles are needed, and how authentication works.
The key components:
- SecurityFilterChain β the filter chain configuration
- AuthenticationManager β verifies credentials
- UserDetailsService β loads user data for authentication
- SecurityContext β holds the authenticated user for the current request
Basic Configuration
java@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // disable for REST APIs using JWT .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() .anyRequest().authenticated() ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } }
JWT Authentication Setup
1. JWT Utility
java@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration:86400000}") private long expiration; // 24 hours in ms public String generateToken(UserDetails userDetails) { return Jwts.builder() .subject(userDetails.getUsername()) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSigningKey()) .compact(); } public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } 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 extractClaim(token, Claims::getExpiration).before(new Date()); } private <T> T extractClaim(String token, Function<Claims, T> resolver) { Claims claims = Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload(); return resolver.apply(claims); } private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(keyBytes); } }
2. JWT Authentication Filter
java@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } final String token = authHeader.substring(7); final String username; try { username = jwtUtil.extractUsername(token); } catch (JwtException e) { filterChain.doFilter(request, response); return; } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.isTokenValid(token, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
3. Register the Filter in SecurityConfig
java@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final UserDetailsService userDetailsService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config ) throws Exception { return config.getAuthenticationManager(); } }
4. UserDetailsService Implementation
java@Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return userRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); } }
Your User entity should implement UserDetails:
java@Entity @Table(name = "users") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String email; private String password; @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 String getPassword() { return password; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired(){ return true; } @Override public boolean isEnabled() { return true; } }
5. Authentication Controller
java@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { private final AuthenticationManager authManager; private final UserDetailsService userDetailsService; private final JwtUtil jwtUtil; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @PostMapping("/login") public ResponseEntity<AuthResponse> login(@RequestBody @Valid LoginRequest req) { authManager.authenticate( new UsernamePasswordAuthenticationToken(req.email(), req.password()) ); UserDetails user = userDetailsService.loadUserByUsername(req.email()); String token = jwtUtil.generateToken(user); return ResponseEntity.ok(new AuthResponse(token)); } @PostMapping("/register") public ResponseEntity<AuthResponse> register(@RequestBody @Valid RegisterRequest req) { if (userRepository.existsByEmail(req.email())) { throw new ResponseStatusException(HttpStatus.CONFLICT, "Email already in use"); } User user = new User(); user.setEmail(req.email()); user.setPassword(passwordEncoder.encode(req.password())); user.setRole(Role.USER); userRepository.save(user); String token = jwtUtil.generateToken(user); return ResponseEntity.status(HttpStatus.CREATED).body(new AuthResponse(token)); } }
Role-Based Authorization
URL-level authorization
java.authorizeHttpRequests(auth -> auth .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER") .requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") .anyRequest().authenticated() )
Method-level security
java@Configuration @EnableMethodSecurity public class SecurityConfig { ... } @Service public class UserService { @PreAuthorize("hasRole('ADMIN')") public List<User> getAllUsers() { ... } @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id") public User getUser(Long userId) { ... } @PostAuthorize("returnObject.email == authentication.name") public User getCurrentUserProfile() { ... } }
@PreAuthorize evaluates before the method runs. @PostAuthorize evaluates after, with access to the return value.
CORS Configuration
java@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("https://myapp.com", "http://localhost:3000")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; }
Testing Secured Endpoints
java@SpringBootTest @AutoConfigureMockMvc class UserControllerTest { @Autowired MockMvc mockMvc; @Test @WithMockUser(roles = "ADMIN") void adminCanGetAllUsers() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isOk()); } @Test @WithMockUser(roles = "USER") void userCannotAccessAdminEndpoint() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isForbidden()); } @Test void unauthenticatedCannotAccessProtectedEndpoint() throws Exception { mockMvc.perform(get("/api/profile")) .andExpect(status().isUnauthorized()); } }
Common Interview Questions
Q: What is the difference between authentication and authorization?
Authentication verifies who you are (login with credentials). Authorization determines what you are allowed to do (access control based on roles/permissions). Spring Security handles both: UserDetailsService for authentication, authorizeHttpRequests and @PreAuthorize for authorization.
Q: Why do you disable CSRF for REST APIs?
CSRF attacks work by tricking a browser into making requests using existing cookies. Since REST APIs authenticate with JWT tokens in the Authorization header (not cookies), the browser cannot automatically include them in cross-site requests β so CSRF protection is unnecessary and just adds overhead.
Q: What is the SecurityContext?
The SecurityContext holds the Authentication object for the current request thread. Spring uses ThreadLocal storage so each request thread has its own security context. You access it via
SecurityContextHolder.getContext().getAuthentication()Practice Java on Froquiz
Spring Security questions are common in Java backend interviews at intermediate and senior levels. Test your Java and Spring Boot knowledge on Froquiz across all difficulty levels.
Summary
- Spring Security intercepts requests through a filter chain before they reach controllers
SecurityFilterChainconfigures URL authorization, session policy, and custom filters- JWT authentication: validate token in a filter, set authentication in
SecurityContext UserDetailsServiceloads user data; theUserentity implementsUserDetails- Use
@PreAuthorizefor method-level security with Spring Expression Language - Configure CORS via
CorsConfigurationSourceregistered in the security config - Disable CSRF for stateless REST APIs using JWT Bearer tokens