Java Programming Hub

Advanced Java development tutorials and guides

Spring SecurityAuthenticationAuthorizationJWTOAuth2

Spring Security 6: Complete Authentication and Authorization Guide

January 10, 202522 min readSecurity
# Spring Security 6: Complete Authentication and Authorization Guide Spring Security 6 introduces significant changes and improvements, including enhanced OAuth2 support, simplified configuration, and better integration with modern authentication standards. This guide covers everything you need to secure your Spring applications effectively. ## Spring Security 6 Overview Spring Security 6 brings several major changes: - Requires Java 17+ - Servlet API 6.0+ support - Enhanced OAuth2 and OpenID Connect support - Simplified configuration with lambda DSL - Improved observability and metrics - Better integration with Spring Boot 3 ## Basic Security Configuration ### Modern Configuration Approach ```java @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers("/api/public/**") ) .authorizeHttpRequests(authz -> authz .requestMatchers("/", "/home", "/public/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .failureUrl("/login?error") .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) .maxSessionsPreventsLogin(false) ) .build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } } ``` ### User Details Service Implementation ```java @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); return CustomUserPrincipal.builder() .id(user.getId()) .username(user.getUsername()) .password(user.getPassword()) .email(user.getEmail()) .authorities(mapRolesToAuthorities(user.getRoles())) .enabled(user.isEnabled()) .accountNonExpired(user.isAccountNonExpired()) .accountNonLocked(user.isAccountNonLocked()) .credentialsNonExpired(user.isCredentialsNonExpired()) .build(); } private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Set<Role> roles) { return roles.stream() .flatMap(role -> { List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())); // Add permissions as authorities role.getPermissions().forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.getName()))); return authorities.stream(); }) .collect(Collectors.toList()); } } @Builder @Data public class CustomUserPrincipal implements UserDetails { private Long id; private String username; private String password; private String email; private Collection<? extends GrantedAuthority> authorities; private boolean enabled; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } // Other UserDetails methods... } ``` ## JWT Authentication Implementation ### JWT Configuration ```java @Component public class JwtTokenProvider { private final String jwtSecret; private final int jwtExpirationMs; private final Key key; public JwtTokenProvider(@Value("${app.jwt.secret}") String jwtSecret, @Value("${app.jwt.expiration}") int jwtExpirationMs) { this.jwtSecret = jwtSecret; this.jwtExpirationMs = jwtExpirationMs; this.key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); } public String generateToken(Authentication authentication) { CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal(); Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationMs); return Jwts.builder() .setSubject(userPrincipal.getUsername()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .claim("userId", userPrincipal.getId()) .claim("authorities", userPrincipal.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())) .signWith(key, SignatureAlgorithm.HS512) .compact(); } public String generateRefreshToken(String username) { Date expiryDate = new Date(System.currentTimeMillis() + (jwtExpirationMs * 7)); // 7x longer return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(expiryDate) .claim("type", "refresh") .signWith(key, SignatureAlgorithm.HS512) .compact(); } public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { logger.error("Invalid JWT token: {}", e.getMessage()); return false; } } public Collection<? extends GrantedAuthority> getAuthoritiesFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); @SuppressWarnings("unchecked") List<String> authorities = claims.get("authorities", List.class); return authorities.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } } ``` ### JWT Authentication Filter ```java @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider tokenProvider; @Autowired private CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getTokenFromRequest(request); if (token != null && tokenProvider.validateToken(token)) { String username = tokenProvider.getUsernameFromToken(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource() .buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` ### Authentication Controller ```java @RestController @RequestMapping("/api/auth") @Validated public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenProvider tokenProvider; @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<JwtAuthenticationResponse> authenticateUser( @Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = tokenProvider.generateToken(authentication); String refreshToken = tokenProvider.generateRefreshToken( loginRequest.getUsername()); return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, refreshToken)); } @PostMapping("/register") public ResponseEntity<ApiResponse> registerUser( @Valid @RequestBody SignUpRequest signUpRequest) { if (userService.existsByUsername(signUpRequest.getUsername())) { return ResponseEntity.badRequest() .body(new ApiResponse(false, "Username is already taken!")); } if (userService.existsByEmail(signUpRequest.getEmail())) { return ResponseEntity.badRequest() .body(new ApiResponse(false, "Email Address already in use!")); } User user = User.builder() .username(signUpRequest.getUsername()) .email(signUpRequest.getEmail()) .password(passwordEncoder.encode(signUpRequest.getPassword())) .roles(Set.of(roleRepository.findByName("ROLE_USER") .orElseThrow(() -> new RuntimeException("User Role not set.")))) .build(); User result = userService.save(user); return ResponseEntity.ok(new ApiResponse(true, "User registered successfully")); } @PostMapping("/refresh") public ResponseEntity<JwtAuthenticationResponse> refreshToken( @Valid @RequestBody TokenRefreshRequest request) { String refreshToken = request.getRefreshToken(); if (tokenProvider.validateToken(refreshToken)) { String username = tokenProvider.getUsernameFromToken(refreshToken); UserDetails userDetails = userDetailsService.loadUserByUsername(username); Authentication authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); String newToken = tokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtAuthenticationResponse(newToken, refreshToken)); } else { throw new TokenRefreshException(refreshToken, "Refresh token is not valid!"); } } } ``` ## OAuth2 and OpenID Connect ### OAuth2 Client Configuration ```yaml # application.yml spring: security: oauth2: client: registration: google: client-id: ${GOOGLE_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} scope: - openid - profile - email github: client-id: ${GITHUB_CLIENT_ID} client-secret: ${GITHUB_CLIENT_SECRET} scope: - user:email - read:user provider: google: authorization-uri: https://accounts.google.com/o/oauth2/v2/auth token-uri: https://oauth2.googleapis.com/token user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo user-name-attribute: email ``` ### OAuth2 Security Configuration ```java @Configuration public class OAuth2SecurityConfig { @Autowired private CustomOAuth2UserService customOAuth2UserService; @Autowired private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; @Autowired private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; @Bean public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(authz -> authz .requestMatchers("/", "/error", "/login**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService) ) .successHandler(oAuth2AuthenticationSuccessHandler) .failureHandler(oAuth2AuthenticationFailureHandler) ) .build(); } } @Service public class CustomOAuth2UserService extends DefaultOAuth2UserService { @Autowired private UserRepository userRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oauth2User = super.loadUser(userRequest); try { return processOAuth2User(userRequest, oauth2User); } catch (AuthenticationException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause()); } } private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) { OAuth2UserInfo oauth2UserInfo = OAuth2UserInfoFactory .getOAuth2UserInfo(userRequest.getClientRegistration().getRegistrationId(), oauth2User.getAttributes()); if (StringUtils.isEmpty(oauth2UserInfo.getEmail())) { throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider"); } Optional<User> userOptional = userRepository.findByEmail(oauth2UserInfo.getEmail()); User user; if (userOptional.isPresent()) { user = userOptional.get(); if (!user.getProvider().equals(AuthProvider.valueOf( userRequest.getClientRegistration().getRegistrationId().toUpperCase()))) { throw new OAuth2AuthenticationProcessingException( "Looks like you're signed up with " + user.getProvider() + " account. Please use your " + user.getProvider() + " account to login."); } user = updateExistingUser(user, oauth2UserInfo); } else { user = registerNewUser(userRequest, oauth2UserInfo); } return UserPrincipal.create(user, oauth2User.getAttributes()); } private User registerNewUser(OAuth2UserRequest userRequest, OAuth2UserInfo oauth2UserInfo) { User user = User.builder() .provider(AuthProvider.valueOf(userRequest.getClientRegistration() .getRegistrationId().toUpperCase())) .providerId(oauth2UserInfo.getId()) .name(oauth2UserInfo.getName()) .email(oauth2UserInfo.getEmail()) .imageUrl(oauth2UserInfo.getImageUrl()) .build(); return userRepository.save(user); } private User updateExistingUser(User existingUser, OAuth2UserInfo oauth2UserInfo) { existingUser.setName(oauth2UserInfo.getName()); existingUser.setImageUrl(oauth2UserInfo.getImageUrl()); return userRepository.save(existingUser); } } ``` ## Method-Level Security ### Method Security Configuration ```java @Configuration @EnableMethodSecurity( prePostEnabled = true, securedEnabled = true, jsr250Enabled = true ) public class MethodSecurityConfig { @Bean public MethodSecurityExpressionHandler methodSecurityExpressionHandler() { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator()); return expressionHandler; } } @Component public class CustomPermissionEvaluator implements PermissionEvaluator { @Autowired private UserService userService; @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { if (authentication == null || targetDomainObject == null || !(permission instanceof String)) { return false; } String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase(); return hasPrivilege(authentication, targetType, permission.toString().toUpperCase()); } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { if (authentication == null || targetType == null || !(permission instanceof String)) { return false; } return hasPrivilege(authentication, targetType.toUpperCase(), permission.toString().toUpperCase()); } private boolean hasPrivilege(Authentication authentication, String targetType, String permission) { CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal(); return userPrincipal.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .anyMatch(authority -> authority.equals(targetType + "_" + permission) || authority.equals("ROLE_ADMIN")); } } ``` ### Method Security Examples ```java @Service public class UserService { @Autowired private UserRepository userRepository; // Pre-authorization @PreAuthorize("hasRole('ADMIN')") public List<User> getAllUsers() { return userRepository.findAll(); } // Post-authorization @PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')") public User getUserById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found")); } // Custom permission evaluation @PreAuthorize("hasPermission(#user, 'WRITE')") public User updateUser(User user) { return userRepository.save(user); } // Method-level filtering @PostFilter("filterObject.department == authentication.principal.department or hasRole('ADMIN')") public List<User> getUsersByDepartment() { return userRepository.findAll(); } // JSR-250 annotations @RolesAllowed({"ADMIN", "MANAGER"}) public void deleteUser(Long id) { userRepository.deleteById(id); } // Spring Security annotations @Secured("ROLE_ADMIN") public void resetPassword(Long userId, String newPassword) { User user = getUserById(userId); user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); } // Complex expressions @PreAuthorize("@userService.isOwnerOrAdmin(#userId, authentication.name)") public void updateUserProfile(Long userId, UserProfileRequest request) { User user = getUserById(userId); // Update user profile userRepository.save(user); } public boolean isOwnerOrAdmin(Long userId, String currentUsername) { User user = getUserById(userId); return user.getUsername().equals(currentUsername) || SecurityContextHolder.getContext().getAuthentication() .getAuthorities().stream() .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN")); } } ``` ## Security Testing ### Security Test Configuration ```java @SpringBootTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @TestPropertySource(locations = "classpath:application-test.properties") public class SecurityIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private JwtTokenProvider tokenProvider; @MockBean private UserDetailsService userDetailsService; @Test @WithMockUser(roles = "USER") public void testUserAccess() throws Exception { mockMvc.perform(get("/api/users/profile")) .andExpect(status().isOk()); } @Test @WithMockUser(roles = "ADMIN") public void testAdminAccess() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isOk()); } @Test public void testUnauthorizedAccess() throws Exception { mockMvc.perform(get("/api/users/profile")) .andExpect(status().isUnauthorized()); } @Test public void testJwtAuthentication() throws Exception { // Create mock user CustomUserPrincipal userPrincipal = CustomUserPrincipal.builder() .username("testuser") .authorities(List.of(new SimpleGrantedAuthority("ROLE_USER"))) .build(); Authentication authentication = new UsernamePasswordAuthenticationToken( userPrincipal, null, userPrincipal.getAuthorities()); String token = tokenProvider.generateToken(authentication); mockMvc.perform(get("/api/users/profile") .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()); } @Test @WithAnonymousUser public void testPublicEndpoint() throws Exception { mockMvc.perform(get("/public/info")) .andExpect(status().isOk()); } } // Custom security test annotation @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) public @interface WithMockCustomUser { String username() default "testuser"; String[] roles() default {"USER"}; String[] authorities() default {}; } public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> { @Override public SecurityContext createSecurityContext(WithMockCustomUser customUser) { SecurityContext context = SecurityContextHolder.createEmptyContext(); List<GrantedAuthority> authorities = new ArrayList<>(); for (String role : customUser.roles()) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } for (String authority : customUser.authorities()) { authorities.add(new SimpleGrantedAuthority(authority)); } CustomUserPrincipal principal = CustomUserPrincipal.builder() .username(customUser.username()) .authorities(authorities) .build(); Authentication auth = new UsernamePasswordAuthenticationToken( principal, null, authorities); context.setAuthentication(auth); return context; } } ``` ## Security Best Practices ### Password Security ```java @Component public class PasswordPolicyValidator { private static final int MIN_LENGTH = 8; private static final String SPECIAL_CHARS = "!@#$%^&*()_+-=[]{}|;:,.<>?"; public ValidationResult validatePassword(String password) { List<String> errors = new ArrayList<>(); if (password == null || password.length() < MIN_LENGTH) { errors.add("Password must be at least " + MIN_LENGTH + " characters long"); } if (!password.matches(".*[A-Z].*")) { errors.add("Password must contain at least one uppercase letter"); } if (!password.matches(".*[a-z].*")) { errors.add("Password must contain at least one lowercase letter"); } if (!password.matches(".*\d.*")) { errors.add("Password must contain at least one digit"); } if (!containsSpecialCharacter(password)) { errors.add("Password must contain at least one special character"); } if (isCommonPassword(password)) { errors.add("Password is too common, please choose a different one"); } return new ValidationResult(errors.isEmpty(), errors); } private boolean containsSpecialCharacter(String password) { return password.chars() .anyMatch(ch -> SPECIAL_CHARS.indexOf(ch) >= 0); } private boolean isCommonPassword(String password) { Set<String> commonPasswords = Set.of( "password", "123456", "password123", "admin", "qwerty" ); return commonPasswords.contains(password.toLowerCase()); } } ``` ### Rate Limiting ```java @Component public class RateLimitingFilter implements Filter { private final Map<String, List<Long>> requestCounts = new ConcurrentHashMap<>(); private final int maxRequests = 100; private final long timeWindow = 60000; // 1 minute @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String clientIp = getClientIp(httpRequest); if (isRateLimited(clientIp)) { httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); httpResponse.getWriter().write("Rate limit exceeded"); return; } chain.doFilter(request, response); } private boolean isRateLimited(String clientIp) { long currentTime = System.currentTimeMillis(); requestCounts.compute(clientIp, (key, timestamps) -> { if (timestamps == null) { timestamps = new ArrayList<>(); } // Remove old timestamps timestamps.removeIf(timestamp -> currentTime - timestamp > timeWindow); // Add current timestamp timestamps.add(currentTime); return timestamps; }); return requestCounts.get(clientIp).size() > maxRequests; } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } } ``` Spring Security 6 provides a robust foundation for securing modern Java applications. By combining proper authentication, authorization, and security best practices, you can build secure, scalable applications that protect user data and system resources effectively.