Bảo Mật (Security) trong Java

1. Nguyên Tắc Bảo Mật

1.1 Mục Tiêu

  • Bảo vệ dữ liệu
  • Xác thực người dùng
  • Phân quyền truy cập
  • Bảo mật truyền thông
  • Logging và Monitoring

1.2 OWASP Top 10

  1. Broken Access Control
  2. Cryptographic Failures
  3. Injection
  4. Insecure Design
  5. Security Misconfiguration
  6. Vulnerable Components
  7. Authentication Failures
  8. Software and Data Integrity Failures
  9. Security Logging and Monitoring Failures
  10. Server-Side Request Forgery

2. Authentication & Authorization

2.1 Spring Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(
            HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**")
                    .hasRole("ADMIN")
                .requestMatchers("/api/user/**")
                    .hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(
                    SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(
                jwtAuthenticationFilter(),
                UsernamePasswordAuthenticationFilter.class
            );

        return http.build();
    }

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

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) 
            throws Exception {
        return config.getAuthenticationManager();
    }
}

2.2 JWT Implementation

@Service
@Slf4j
public class JwtService {
    @Value("${jwt.secret}")
    private String secretKey;

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

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));

        return Jwts.builder()
            .setClaims(claims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() 
                + jwtExpiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            log.error("Invalid JWT token", e);
            return false;
        }
    }

    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64
            .decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

@Component
public class JwtAuthenticationFilter extends 
        OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserDetailsService userDetailsService;

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

        final String authHeader = request
            .getHeader("Authorization");
        final String jwt;
        final String username;

        if (authHeader == null || 
            !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        jwt = authHeader.substring(7);
        username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder
                .getContext().getAuthentication() == null) {

            UserDetails userDetails = userDetailsService
                .loadUserByUsername(username);

            if (jwtService.validateToken(jwt)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                    );

                authToken.setDetails(new WebAuthenticationDetailsSource()
                    .buildDetails(request));

                SecurityContextHolder.getContext()
                    .setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

3. Cryptography

3.1 Password Hashing

@Service
public class PasswordService {
    private final PasswordEncoder passwordEncoder;

    public PasswordService(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    public String hashPassword(String rawPassword) {
        return passwordEncoder.encode(rawPassword);
    }

    public boolean verifyPassword(
            String rawPassword, 
            String hashedPassword) {
        return passwordEncoder.matches(
            rawPassword, hashedPassword);
    }
}

@Service
public class UserService {
    private final PasswordService passwordService;
    private final UserRepository userRepository;

    public void registerUser(UserRegistrationRequest request) {
        // Validate password strength
        validatePasswordStrength(request.getPassword());

        // Hash password
        String hashedPassword = passwordService
            .hashPassword(request.getPassword());

        // Create user
        User user = User.builder()
            .username(request.getUsername())
            .password(hashedPassword)
            .build();

        userRepository.save(user);
    }

    private void validatePasswordStrength(String password) {
        if (password.length() < 8) {
            throw new ValidationException(
                "Password must be at least 8 characters");
        }

        if (!password.matches(".*[A-Z].*")) {
            throw new ValidationException(
                "Password must contain uppercase letter");
        }

        if (!password.matches(".*[a-z].*")) {
            throw new ValidationException(
                "Password must contain lowercase letter");
        }

        if (!password.matches(".*\\d.*")) {
            throw new ValidationException(
                "Password must contain digit");
        }

        if (!password.matches(".*[!@#$%^&*()].*")) {
            throw new ValidationException(
                "Password must contain special character");
        }
    }
}

3.2 Data Encryption

@Service
public class EncryptionService {
    @Value("${encryption.key}")
    private String secretKey;

    private final String ALGORITHM = "AES/GCM/NoPadding";
    private final int TAG_LENGTH_BIT = 128;
    private final int IV_LENGTH_BYTE = 12;

    public String encrypt(String data) throws Exception {
        byte[] iv = generateIv();
        SecretKey key = generateKey();

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec spec = new GCMParameterSpec(
            TAG_LENGTH_BIT, iv);

        cipher.init(Cipher.ENCRYPT_MODE, key, spec);
        byte[] encryptedData = cipher.doFinal(
            data.getBytes());

        byte[] message = ByteBuffer.allocate(
                iv.length + encryptedData.length)
            .put(iv)
            .put(encryptedData)
            .array();

        return Base64.getEncoder()
            .encodeToString(message);
    }

    public String decrypt(String encryptedData) 
            throws Exception {
        byte[] decoded = Base64.getDecoder()
            .decode(encryptedData);

        ByteBuffer bb = ByteBuffer.wrap(decoded);
        byte[] iv = new byte[IV_LENGTH_BYTE];
        bb.get(iv);

        byte[] cipherText = new byte[
            bb.remaining()];
        bb.get(cipherText);

        SecretKey key = generateKey();
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec spec = new GCMParameterSpec(
            TAG_LENGTH_BIT, iv);

        cipher.init(Cipher.DECRYPT_MODE, key, spec);
        byte[] decryptedData = cipher.doFinal(
            cipherText);

        return new String(decryptedData);
    }

    private SecretKey generateKey() {
        byte[] decodedKey = Base64.getDecoder()
            .decode(secretKey);
        return new SecretKeySpec(
            decodedKey, 0, decodedKey.length, "AES");
    }

    private byte[] generateIv() {
        byte[] iv = new byte[IV_LENGTH_BYTE];
        new SecureRandom().nextBytes(iv);
        return iv;
    }
}

@Entity
public class SensitiveData {
    @Id
    private Long id;

    @Convert(converter = EncryptedStringConverter.class)
    private String creditCardNumber;

    // Other fields
}

@Converter
public class EncryptedStringConverter implements 
        AttributeConverter<String, String> {

    @Autowired
    private EncryptionService encryptionService;

    @Override
    public String convertToDatabaseColumn(String attribute) {
        try {
            return attribute != null ? 
                encryptionService.encrypt(attribute) : null;
        } catch (Exception e) {
            throw new RuntimeException(
                "Error encrypting data", e);
        }
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        try {
            return dbData != null ? 
                encryptionService.decrypt(dbData) : null;
        } catch (Exception e) {
            throw new RuntimeException(
                "Error decrypting data", e);
        }
    }
}

4. Input Validation

4.1 Request Validation

@Validated
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @RequestBody @Valid UserRequest request) {
        // Implementation
    }
}

@Data
@Builder
public class UserRequest {
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 50, 
        message = "Username must be between 3 and 50 characters")
    @Pattern(regexp = "^[a-zA-Z0-9._-]+$", 
        message = "Username can only contain letters, numbers, dots, underscores and hyphens")
    private String username;

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

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

    @NotNull(message = "Role is required")
    @Enumerated(EnumType.STRING)
    private UserRole role;
}

@ControllerAdvice
public class ValidationExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();

        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        return ResponseEntity
            .badRequest()
            .body(new ErrorResponse(
                "Validation failed", 
                errors));
    }
}

4.2 SQL Injection Prevention

@Repository
public class UserRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // Bad practice - vulnerable to SQL injection
    public User findByUsername(String username) {
        String sql = "SELECT * FROM users WHERE username = '" + 
            username + "'";
        return jdbcTemplate.queryForObject(
            sql, new UserRowMapper());
    }

    // Good practice - using prepared statement
    public User findByUsernameSecure(String username) {
        String sql = "SELECT * FROM users WHERE username = ?";
        return jdbcTemplate.queryForObject(
            sql, 
            new Object[]{username}, 
            new UserRowMapper());
    }

    // Better practice - using JPA
    @Query("SELECT u FROM User u WHERE u.username = :username")
    Optional<User> findByUsername(
        @Param("username") String username);
}

// XSS Prevention
@Component
public class XssFilter implements Filter {
    @Override
    public void doFilter(
            ServletRequest request, 
            ServletResponse response, 
            FilterChain chain) 
            throws IOException, ServletException {
        chain.doFilter(
            new XssRequestWrapper((HttpServletRequest) request), 
            response);
    }
}

public class XssRequestWrapper extends 
        HttpServletRequestWrapper {

    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String[] getParameterValues(String parameter) {
        String[] values = super.getParameterValues(parameter);
        if (values == null) {
            return null;
        }

        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = stripXSS(values[i]);
        }

        return encodedValues;
    }

    private String stripXSS(String value) {
        if (value != null) {
            value = value.replaceAll("<", "&lt;")
                        .replaceAll(">", "&gt;");
            value = Jsoup.clean(
                value, 
                Whitelist.none());
        }
        return value;
    }
}

5. Secure Communication

5.1 HTTPS Configuration

@Configuration
public class ServerConfig {
    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = 
            new TomcatServletWebServerFactory();
        tomcat.addAdditionalTomcatConnectors(
            redirectConnector());
        return tomcat;
    }

    private Connector redirectConnector() {
        Connector connector = new Connector(
            TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(8443);
        return connector;
    }
}

# application.properties
server.port=8443
server.ssl.key-store-type=PKCS12
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${KEYSTORE_PASSWORD}
server.ssl.key-alias=tomcat
security.require-ssl=true

5.2 API Security

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(
            HttpSecurity http) throws Exception {
        http
            .requiresChannel()
                .requestMatchers("/api/**")
                    .requiresSecure()
            .and()
            .headers()
                .frameOptions()
                    .deny()
                .xssProtection()
                    .block(true)
                .contentSecurityPolicy(
                    "default-src 'self'")
            .and()
            .cors()
                .configurationSource(corsConfigurationSource())
            .and()
            .csrf()
                .csrfTokenRepository(
                    CookieCsrfTokenRepository.withHttpOnlyFalse());

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = 
            new CorsConfiguration();
        configuration.setAllowedOrigins(
            Arrays.asList("https://trusted-domain.com"));
        configuration.setAllowedMethods(
            Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(
            Arrays.asList("Authorization", "Content-Type"));
        configuration.setExposedHeaders(
            Arrays.asList("X-Custom-Header"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

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

6. Logging và Monitoring

6.1 Security Logging

@Aspect
@Component
@Slf4j
public class SecurityAuditAspect {
    @Autowired
    private AuditLogRepository auditLogRepository;

    @Around("@annotation(Audited)")
    public Object auditMethod(ProceedingJoinPoint joinPoint) 
            throws Throwable {
        String methodName = joinPoint.getSignature()
            .getName();
        String className = joinPoint.getTarget()
            .getClass().getName();

        Authentication authentication = 
            SecurityContextHolder.getContext()
                .getAuthentication();
        String username = authentication != null ? 
            authentication.getName() : "anonymous";

        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = joinPoint.proceed();
            logSuccess(className, methodName, username);
            return result;
        } catch (Exception e) {
            logFailure(className, methodName, username, e);
            throw e;
        } finally {
            long duration = System.currentTimeMillis() - 
                startTime;
            saveAuditLog(className, methodName, username, 
                duration, result);
        }
    }

    private void logSuccess(
            String className, 
            String methodName, 
            String username) {
        log.info("User {} successfully executed {}.{}", 
            username, className, methodName);
    }

    private void logFailure(
            String className, 
            String methodName, 
            String username, 
            Exception e) {
        log.error("User {} failed to execute {}.{}: {}", 
            username, className, methodName, 
            e.getMessage());
    }

    private void saveAuditLog(
            String className, 
            String methodName, 
            String username, 
            long duration, 
            Object result) {
        AuditLog auditLog = AuditLog.builder()
            .className(className)
            .methodName(methodName)
            .username(username)
            .duration(duration)
            .result(result != null ? 
                result.toString() : null)
            .timestamp(LocalDateTime.now())
            .build();

        auditLogRepository.save(auditLog);
    }
}

6.2 Security Monitoring

@Configuration
public class SecurityMetricsConfig {
    @Bean
    public MeterRegistry meterRegistry() {
        return new SimpleMeterRegistry();
    }
}

@Component
@Slf4j
public class SecurityMetrics {
    private final MeterRegistry meterRegistry;
    private final Counter loginFailures;
    private final Counter accessDenied;
    private final Timer authenticationTime;

    public SecurityMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;

        this.loginFailures = Counter.builder("security.login.failures")
            .description("Number of failed login attempts")
            .register(meterRegistry);

        this.accessDenied = Counter.builder("security.access.denied")
            .description("Number of access denied events")
            .register(meterRegistry);

        this.authenticationTime = Timer.builder("security.auth.time")
            .description("Time taken for authentication")
            .register(meterRegistry);
    }

    public void recordLoginFailure(String username) {
        loginFailures.increment();
        log.warn("Failed login attempt for user: {}", username);
    }

    public void recordAccessDenied(String username, String resource) {
        accessDenied.increment();
        log.warn("Access denied for user: {} to resource: {}", 
            username, resource);
    }

    public Timer.Sample startAuthenticationTimer() {
        return Timer.start(meterRegistry);
    }

    public void stopAuthenticationTimer(Timer.Sample sample) {
        sample.stop(authenticationTime);
    }
}

@Component
public class SecurityEventListener {
    @Autowired
    private SecurityMetrics securityMetrics;

    @EventListener
    public void handleAuthenticationFailure(
            AuthenticationFailureBadCredentialsEvent event) {
        String username = event.getAuthentication()
            .getName();
        securityMetrics.recordLoginFailure(username);
    }

    @EventListener
    public void handleAccessDenied(
            AccessDeniedEvent event) {
        Authentication auth = event.getAuthentication();
        String username = auth != null ? 
            auth.getName() : "anonymous";
        String resource = event.getAccessDeniedException()
            .getMessage();
        securityMetrics.recordAccessDenied(
            username, resource);
    }
}

7. Security Testing

7.1 Unit Testing

@ExtendWith(MockitoExtension.class)
class SecurityServiceTest {
    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private SecurityService securityService;

    @Test
    void whenValidCredentials_thenAuthenticate() {
        // Given
        String username = "user";
        String password = "password";
        String hashedPassword = "hashedPassword";

        User user = User.builder()
            .username(username)
            .password(hashedPassword)
            .build();

        when(userRepository.findByUsername(username))
            .thenReturn(Optional.of(user));
        when(passwordEncoder.matches(password, hashedPassword))
            .thenReturn(true);

        // When
        boolean result = securityService
            .authenticate(username, password);

        // Then
        assertTrue(result);
        verify(userRepository).findByUsername(username);
        verify(passwordEncoder).matches(
            password, hashedPassword);
    }

    @Test
    void whenInvalidCredentials_thenThrowException() {
        // Given
        String username = "user";
        String password = "wrongPassword";

        when(userRepository.findByUsername(username))
            .thenReturn(Optional.empty());

        // When/Then
        assertThrows(AuthenticationException.class, () ->
            securityService.authenticate(username, password));
    }
}

7.2 Integration Testing

@SpringBootTest(webEnvironment = RANDOM_PORT)
class SecurityIntegrationTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private JwtService jwtService;

    @Test
    void whenAccessProtectedEndpoint_withoutToken_thenUnauthorized() {
        // When
        ResponseEntity<String> response = restTemplate
            .getForEntity("/api/protected", String.class);

        // Then
        assertEquals(HttpStatus.UNAUTHORIZED, 
            response.getStatusCode());
    }

    @Test
    void whenAccessProtectedEndpoint_withValidToken_thenSuccess() {
        // Given
        String token = jwtService.generateToken("user");
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);

        // When
        ResponseEntity<String> response = restTemplate
            .exchange(
                "/api/protected",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class
            );

        // Then
        assertEquals(HttpStatus.OK, 
            response.getStatusCode());
    }
}

8. Security Best Practices

8.1 Error Handling

@ControllerAdvice
public class SecurityExceptionHandler {
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDeniedException(
            AccessDeniedException ex) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.FORBIDDEN.value())
            .error("Access Denied")
            .message("You don't have permission to access this resource")
            .timestamp(LocalDateTime.now())
            .build();

        return new ResponseEntity<>(error, 
            HttpStatus.FORBIDDEN);
    }

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<ErrorResponse> handleAuthenticationException(
            AuthenticationException ex) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.UNAUTHORIZED.value())
            .error("Authentication Failed")
            .message("Invalid credentials")
            .timestamp(LocalDateTime.now())
            .build();

        return new ResponseEntity<>(error, 
            HttpStatus.UNAUTHORIZED);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(
            Exception ex) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error("Internal Server Error")
            .message("An unexpected error occurred")
            .timestamp(LocalDateTime.now())
            .build();

        return new ResponseEntity<>(error, 
            HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

8.2 Security Headers

@Configuration
public class SecurityHeadersConfig {
    @Bean
    public FilterRegistrationBean<Filter> securityHeadersFilter() {
        FilterRegistrationBean<Filter> registrationBean = 
            new FilterRegistrationBean<>();

        registrationBean.setFilter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    FilterChain filterChain)
                    throws ServletException, IOException {

                response.setHeader("X-Content-Type-Options", "nosniff");
                response.setHeader("X-Frame-Options", "DENY");
                response.setHeader("X-XSS-Protection", "1; mode=block");
                response.setHeader("Strict-Transport-Security", 
                    "max-age=31536000; includeSubDomains");
                response.setHeader("Cache-Control", 
                    "no-cache, no-store, must-revalidate");
                response.setHeader("Pragma", "no-cache");
                response.setHeader("Expires", "0");
                response.setHeader("Content-Security-Policy",
                    "default-src 'self'; " +
                    "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
                    "style-src 'self' 'unsafe-inline';");

                filterChain.doFilter(request, response);
            }
        });

        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);

        return registrationBean;
    }
}

9. References và Further Reading

9.1 Books

  • Spring Security in Action
  • Java Security Handbook
  • OWASP Testing Guide
  • Web Application Security
  • Cryptography Engineering

9.2 Online Resources