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
- Broken Access Control
- Cryptographic Failures
- Injection
- Insecure Design
- Security Misconfiguration
- Vulnerable Components
- Authentication Failures
- Software and Data Integrity Failures
- Security Logging and Monitoring Failures
- 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.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("<", "<")
.replaceAll(">", ">");
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);
}
}
@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