Spring Security

libraryauthenticationsecurityJavaSpring BootOAuth2JWTauthorizationweb security

Authentication Library

Spring Security

Overview

Spring Security is a comprehensive security framework for Java applications, particularly Spring Boot applications. It provides powerful and highly customizable authentication and authorization capabilities, supporting various security standards including OAuth2, JWT, SAML, and OpenID Connect. Spring Security offers protection against common security vulnerabilities and provides enterprise-grade security features.

Details

Spring Security is the de facto standard for security in the Spring ecosystem, offering a flexible and extensible security framework. It provides declarative security configuration through annotations and XML, supports multiple authentication mechanisms, and integrates seamlessly with Spring Boot's auto-configuration.

Key architectural components include SecurityFilterChain for request processing, AuthenticationManager for user authentication, and AccessDecisionManager for authorization decisions. The framework supports method-level security, URL-based security, and fine-grained access control.

Spring Security 6.x (compatible with Spring Boot 3.x) introduces significant improvements including enhanced OAuth2 support, improved JWT handling, and streamlined configuration options. It maintains backward compatibility while providing modern security features for cloud-native applications.

Advantages and Disadvantages

Advantages

  • Comprehensive Security Features: Covers authentication, authorization, CSRF protection, session management, and more
  • Spring Ecosystem Integration: Seamless integration with Spring Boot and other Spring projects
  • Multiple Authentication Methods: Supports database, LDAP, OAuth2, JWT, and custom authentication providers
  • Enterprise Ready: Proven in enterprise environments with extensive customization options
  • Active Development: Regular updates with latest security standards and vulnerability patches
  • Extensive Documentation: Comprehensive official documentation and community resources

Disadvantages

  • Complex Configuration: Can be overwhelming for beginners with many configuration options
  • Learning Curve: Requires understanding of security concepts and Spring framework
  • Heavy Framework: May be overkill for simple applications with basic security needs
  • Debugging Complexity: Security filter chains can be difficult to debug when issues arise
  • Version Compatibility: Breaking changes between major versions require careful migration

Reference Links

Code Examples

Installation and Spring Boot Configuration

<!-- Maven dependency for Spring Boot 3.x -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.2.0</version>
</dependency>

<!-- Additional OAuth2 support -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
// Basic Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults())
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
        return http.build();
    }

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

Basic Authentication and User Management

// In-Memory User Details Service
@Configuration
public class UserConfig {

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("password"))
            .roles("USER")
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder.encode("admin"))
            .roles("ADMIN", "USER")
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

// Custom User Details Service with Database
@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: " + username));

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .collect(Collectors.toList())
            )
            .accountExpired(!user.isAccountNonExpired())
            .accountLocked(!user.isAccountNonLocked())
            .credentialsExpired(!user.isCredentialsNonExpired())
            .disabled(!user.isEnabled())
            .build();
    }
}

OAuth2/JWT Integration

// JWT Configuration
@Configuration
public class JwtConfig {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Bean
    public JwtDecoder jwtDecoder() {
        SecretKeySpec secretKey = new SecretKeySpec(
            jwtSecret.getBytes(), "HmacSHA256"
        );
        return NimbusJwtDecoder.withSecretKey(secretKey)
            .macAlgorithm(MacAlgorithm.HS256)
            .build();
    }

    @Bean
    public JwtEncoder jwtEncoder() {
        JWK jwk = new OctetSequenceKey.Builder(jwtSecret.getBytes())
            .algorithm(JWSAlgorithm.HS256)
            .build();
        JWKSet jwkSet = new JWKSet(jwk);
        return new NimbusJwtEncoder(new ImmutableJWKSet<>(jwkSet));
    }
}

// JWT Authentication Service
@Service
public class JwtAuthenticationService {

    @Autowired
    private JwtEncoder jwtEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    public String authenticate(String username, String password) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(username, password)
        );

        return generateToken(authentication);
    }

    private String generateToken(Authentication authentication) {
        Instant now = Instant.now();
        long expiry = 3600L; // 1 hour

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("spring-security-app")
            .issuedAt(now)
            .expiresAt(now.plusSeconds(expiry))
            .subject(authentication.getName())
            .claim("authorities", authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList())
            )
            .build();

        return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}

Method-Level Security

// Service with Method Security
@Service
@PreAuthorize("hasRole('USER')")
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
    public User getUserByUsername(String username) {
        // Method implementation
        return userRepository.findByUsername(username);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public User updateUser(User user) {
        return userRepository.save(user);
    }

    @Secured({"ROLE_ADMIN", "ROLE_MODERATOR"})
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }

    @RolesAllowed("ADMIN")
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

// Custom Security Expression
@Component("userSecurity")
public class UserSecurityExpression {

    public boolean isOwner(Authentication authentication, Long userId) {
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }
        
        // Check if the authenticated user is the owner
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        // Custom logic to verify ownership
        return true; // Implement actual ownership check
    }
}

// Usage in controller
@RestController
public class UserController {

    @GetMapping("/users/{id}")
    @PreAuthorize("@userSecurity.isOwner(authentication, #id) or hasRole('ADMIN')")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        // Controller implementation
        return ResponseEntity.ok(userService.findById(id));
    }
}

REST API Authentication

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

    @Autowired
    private JwtAuthenticationService jwtService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        try {
            String token = jwtService.authenticate(
                loginRequest.getUsername(), 
                loginRequest.getPassword()
            );
            
            return ResponseEntity.ok(new JwtResponse(token, "Bearer", 3600));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(new ErrorResponse("Invalid credentials"));
        }
    }

    @PostMapping("/refresh")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<?> refreshToken(Authentication authentication) {
        String newToken = jwtService.generateRefreshToken(authentication);
        return ResponseEntity.ok(new JwtResponse(newToken, "Bearer", 3600));
    }
}

// Custom Authentication Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtDecoder jwtDecoder;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        
        String token = extractTokenFromRequest(request);
        
        if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            try {
                Jwt jwt = jwtDecoder.decode(token);
                Authentication authentication = createAuthentication(jwt);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (JwtException e) {
                logger.error("JWT validation failed: " + e.getMessage());
            }
        }
        
        filterChain.doFilter(request, response);
    }

    private String extractTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    private Authentication createAuthentication(Jwt jwt) {
        Collection<GrantedAuthority> authorities = ((List<String>) jwt.getClaim("authorities"))
            .stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        return new JwtAuthenticationToken(jwt, authorities);
    }
}

Testing and Security Configuration

// Security Test Configuration
@TestConfiguration
@EnableWebSecurity
public class TestSecurityConfig {

    @Bean
    @Primary
    public UserDetailsService testUserDetailsService() {
        UserDetails testUser = User.builder()
            .username("testuser")
            .password("{noop}password") // NoOp encoder for testing
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(testUser);
    }
}

// Integration Tests
@SpringBootTest
@AutoConfigureTestDatabase
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "jwt.secret=testsecretfortesting"
})
class SecurityIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldAuthenticateValidUser() {
        LoginRequest request = new LoginRequest("testuser", "password");
        
        ResponseEntity<JwtResponse> response = restTemplate.postForEntity(
            "/api/auth/login", request, JwtResponse.class
        );
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().getToken()).isNotNull();
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void shouldAllowAdminAccess() {
        ResponseEntity<String> response = restTemplate.getForEntity(
            "/api/admin/users", String.class
        );
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

// Unit Tests with Security Context
@ExtendWith(SpringExtension.class)
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    void shouldAllowAdminToGetAnyUser() throws Exception {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    void shouldDenyRegularUserAccessToOtherUsers() throws Exception {
        mockMvc.perform(get("/api/users/2"))
            .andExpect(status().isForbidden());
    }
}