Advanced Java

Security Handling

Module 8 - Spring Security and Application Protection

Module Objectives

By the end of this module, you will:

Why Application Security Matters

Security Threats Are Real:

Security is not optional. It must be built into the application from the start, not added as an afterthought.

Spring Security Overview

Spring Security is the de-facto standard for securing Spring applications:

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

Spring Security Architecture

flowchart LR
    subgraph Request["HTTP Request"]
        R[Client Request]
    end
    subgraph Filters["Security Filter Chain"]
        F1[Authentication Filter]
        F2[Authorization Filter]
        F3[Exception Filter]
    end
    subgraph Core["Security Core"]
        AM[Authentication Manager]
        SM[Security Context]
    end
    subgraph App["Application"]
        C[Controller]
    end
    R --> F1
    F1 --> AM
    AM --> SM
    F1 --> F2
    F2 --> F3
    F3 --> C
    

Basic Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth // (#1:Define access rules)
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form // (#2:Enable form login)
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            )
            .logout(logout -> logout // (#3:Configure logout)
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            );
        return http.build();
    }
}

Custom UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

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

        return org.springframework.security.core.userdetails.User // (#2:Build UserDetails)
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

Password Encoding

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // (#1:BCrypt is recommended)
    }
}

@Service
public class UserService {

    private final PasswordEncoder passwordEncoder;

    public User createUser(String username, String rawPassword) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(rawPassword)); // (#2:Hash password)
        return userRepository.save(user);
    }
}

Never store plain text passwords! Always use a strong hashing algorithm like BCrypt.

Method-Level Security

@Configuration
@EnableMethodSecurity // (#1:Enable method security)
public class MethodSecurityConfig {}

@Service
public class ProductService {

    @PreAuthorize("hasRole('ADMIN')") // (#2:Check before execution)
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }

    @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id") // (#3:Custom SpEL)
    public List<Order> getOrdersForUser(Long userId) {
        return orderRepository.findByUserId(userId);
    }

    @PostAuthorize("returnObject.owner == authentication.name") // (#4:Check after execution)
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElseThrow();
    }
}

JWT Token Authentication

JSON Web Tokens (JWT) are ideal for stateless REST API authentication:

@Component
public class JwtTokenProvider {

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

    @Value("${jwt.expiration}")
    private long jwtExpiration;

    public String generateToken(Authentication auth) {
        UserDetails user = (UserDetails) auth.getPrincipal();
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);

        return Jwts.builder() // (#1:Build JWT)
            .setSubject(user.getUsername())
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret) // (#2:Sign with secret)
            .compact();
    }
}

JWT Authentication Filter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = getJwtFromRequest(request); // (#1:Extract token)

        if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
            String username = tokenProvider.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken auth = // (#2:Create auth token)
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(auth); // (#3:Set in context)
        }

        filterChain.doFilter(request, response);
    }
}

JWT Security Configuration

@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // (#1:Disable CSRF for stateless API)
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // (#2:No sessions)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, // (#3:Add JWT filter)
                UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

OAuth2 Social Login

@Configuration
@EnableWebSecurity
public class OAuth2Config {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth -> oauth // (#1:Enable OAuth2 login)
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService) // (#2:Custom user service)
                )
            );
        return http.build();
    }
}
# application.properties
spring.security.oauth2.client.registration.google.client-id=your-client-id
spring.security.oauth2.client.registration.google.client-secret=your-secret

CORS Configuration

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of( // (#1:Allowed origins)
            "https://frontend.example.com"
        ));
        configuration.setAllowedMethods(List.of( // (#2:Allowed methods)
            "GET", "POST", "PUT", "DELETE", "OPTIONS"
        ));
        configuration.setAllowedHeaders(List.of("*")); // (#3:Allowed headers)
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);
        return source;
    }
}

CSRF Protection

Cross-Site Request Forgery protection is enabled by default:

@Configuration
public class CsrfConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // (#1:Cookie-based token)
                .ignoringRequestMatchers("/api/webhooks/**") // (#2:Exclude webhooks)
            );
        return http.build();
    }
}

CSRF is important for browser-based apps. For stateless REST APIs with JWT, CSRF can be disabled since tokens already prevent cross-site attacks.

Security Headers

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp // (#1:CSP header)
                .policyDirectives("default-src 'self'; script-src 'self'")
            )
            .frameOptions(frame -> frame.deny()) // (#2:Prevent clickjacking)
            .xssProtection(xss -> xss.disable()) // (#3:Modern browsers don't need this)
            .contentTypeOptions(Customizer.withDefaults()) // (#4:Prevent MIME sniffing)
            .httpStrictTransportSecurity(hsts -> hsts // (#5:Force HTTPS)
                .maxAgeInSeconds(31536000)
                .includeSubDomains(true)
            )
        );
    return http.build();
}

Input Validation

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public ResponseEntity<User> createUser(
            @Valid @RequestBody UserRequest request) { // (#1:Validate input)
        return ResponseEntity.ok(userService.create(request));
    }
}

public class UserRequest {
    @NotBlank(message = "Username is required") // (#2:Validation annotations)
    @Size(min = 3, max = 50)
    @Pattern(regexp = "^[a-zA-Z0-9_]+$")
    private String username;

    @NotBlank
    @Email(message = "Invalid email format")
    private String email;

    @NotBlank
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;
}

Preventing SQL Injection

// VULNERABLE - Never do this!
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
List<User> findByNameUnsafe(String name);

// SAFE - Use parameterized queries
@Query("SELECT u FROM User u WHERE u.name = :name") // (#1:Named parameter)
List<User> findByName(@Param("name") String name);

// SAFE - Use Spring Data methods
List<User> findByUsername(String username); // (#2:Spring Data magic)

// SAFE - Use Criteria API
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
query.where(cb.equal(root.get("name"), name)); // (#3:Type-safe criteria)

Always use parameterized queries! Never concatenate user input into SQL.

Preventing XSS Attacks

Best Practices:

<!-- Thymeleaf - Safe (escapes HTML) -->
<p th:text="${userInput}"></p>

<!-- Thymeleaf - UNSAFE (raw HTML) -->
<p th:utext="${userInput}"></p>

<!-- Use utext ONLY for trusted, sanitized content -->

Rate Limiting

@Component
public class RateLimitingFilter extends OncePerRequestFilter {

    private final RateLimiter rateLimiter = RateLimiter.create(10.0); // (#1:10 requests/second)

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        if (!rateLimiter.tryAcquire()) { // (#2:Check rate limit)
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("Rate limit exceeded");
            return;
        }

        filterChain.doFilter(request, response);
    }
}

Consider using Redis for distributed rate limiting across multiple instances

Security Audit Logging

@Component
public class SecurityAuditListener {

    private static final Logger auditLog = LoggerFactory.getLogger("SECURITY_AUDIT");

    @EventListener
    public void onAuthSuccess(AuthenticationSuccessEvent event) { // (#1:Log success)
        auditLog.info("LOGIN_SUCCESS user={} ip={}",
            event.getAuthentication().getName(),
            getClientIp());
    }

    @EventListener
    public void onAuthFailure(AuthenticationFailureBadCredentialsEvent event) { // (#2:Log failure)
        auditLog.warn("LOGIN_FAILURE user={} ip={}",
            event.getAuthentication().getName(),
            getClientIp());
    }

    @EventListener
    public void onAccessDenied(AuthorizationDeniedEvent event) { // (#3:Log access denied)
        auditLog.warn("ACCESS_DENIED user={} resource={}",
            event.getAuthentication().getName(),
            event.getSource());
    }
}

Secrets Management

Never hardcode secrets!

// BAD - Hardcoded secret
private static final String SECRET = "my-super-secret-key";

// GOOD - Environment variable
@Value("${jwt.secret}")
private String jwtSecret;

Options for managing secrets:

Security Testing

@SpringBootTest
@AutoConfigureMockMvc
class SecurityTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void unauthenticatedAccessShouldBeRejected() throws Exception {
        mockMvc.perform(get("/api/users")) // (#1:Test without auth)
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(roles = "ADMIN") // (#2:Mock authenticated user)
    void adminShouldAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(roles = "USER")
    void userShouldNotAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users")) // (#3:Test authorization)
            .andExpect(status().isForbidden());
    }
}

OWASP Top 10 Checklist

Vulnerability Prevention
Injection Parameterized queries, input validation
Broken Authentication Strong passwords, MFA, session management
Sensitive Data Exposure Encryption at rest and in transit
XXE Disable external entities in XML parsers
Broken Access Control Role-based authorization, principle of least privilege
Security Misconfiguration Secure defaults, remove debug features

Security Best Practices

Summary

In this module, you learned:

Next Module: API Documentation with OpenAPI/Swagger

Resources

Slide Overview