Chương 01: Authentication (Xác thực danh tính)
Học cách nhận diện và bảo vệ danh tính người dùng theo chuẩn NIST và OWASP bằng Java & Spring Boot: Trọn bộ 11 khái niệm từ chính sách mật khẩu, băm Argon2id/BCrypt, MFA TOTP, bảo vệ Session, JWT JJWT, Token Rotation, Smart Lockout đến Rate Limiting HTTP 429.
#01 Password Security (Độ mạnh mật khẩu)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Mật khẩu giống như chiếc chìa khóa cơ của căn nhà bạn. Nếu chìa chỉ có 4 rãnh đơn giản ('admin123'), thợ khóa bất kỳ chỉ cần một thanh kim loại gẩy nhẹ 1 giây là mở được. Nhưng nếu chìa khóa có độ dài và nhiều bi chốt phức tạp (tối thiểu 12 ký tự, hoa, thường, số, ký tự đặc biệt), thời gian để thử từng chiếc chìa giả sẽ tăng từ vài giây lên đến hàng chục ngàn năm.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Password Security (Độ mạnh mật khẩu)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
123456admin123P@ssw0rd2026Enterprise!qwerty
⚠️ Mật khẩu 'admin123' quá yếu! Độ dài 8 ký tự, entropy thấp. Có thể bị bẻ khóa bằng từ điển trong 0.02 giây!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// KHÔNG AN TOÀN TRONG JAVA: Không kiểm tra độ mạnh, cho phép mật khẩu ngắn
@PostMapping("/register")
public ResponseEntity register(@RequestBody UserDto dto) {
if (dto.getPassword() == null || dto.getPassword().length() < 4) {
return ResponseEntity.badRequest().body("Mật khẩu quá ngắn");
}
userService.create(dto.getUsername(), dto.getPassword());
return ResponseEntity.ok("Success");
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN JAVA: Dùng thư viện Passay kiểm tra Entropy & Danh sách đen
import org.passay.*;
@PostMapping("/register")
public ResponseEntity> register(@Valid @RequestBody UserRegistrationRequest request) {
PasswordValidator validator = new PasswordValidator(Arrays.asList(
new LengthRule(12, 128),
new CharacterRule(EnglishCharacterData.UpperCase, 1),
new CharacterRule(EnglishCharacterData.LowerCase, 1),
new CharacterRule(EnglishCharacterData.Digit, 1),
new CharacterRule(EnglishCharacterData.Special, 1),
new WhitespaceRule()
));
RuleResult result = validator.validate(new PasswordData(request.getPassword()));
if (!result.isValid()) {
return ResponseEntity.badRequest().body(validator.getMessages(result));
}
userService.createSecureUser(request);
return ResponseEntity.ok(Map.of("message", "Đăng ký thành công"));
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Theo khuyến nghị NIST SP 800-63B mới nhất, yếu tố nào quan trọng nhất đối với độ an toàn của mật khẩu?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#02 Password Hashing with Argon2id & BCrypt (Băm mật khẩu)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Băm mật khẩu (Hashing) giống như việc bạn xay quả bơ, sữa và đá thành một ly sinh tố bơ. Bạn không bao giờ có thể 'ngược dòng thời gian' để tách ly sinh tố đó trở lại thành quả bơ nguyên vẹn (hàm một chiều). Tuy nhiên, nếu hai người cùng xay một loại bơ giống hệt nhau, ly sinh tố sẽ có màu sắc và vị giống hệt nhau (nguy cơ Rainbow Table). Vì vậy, ta phải thêm 'muối' (Salt) độc nhất cho mỗi người để không ly nào giống ly nào.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Password Hashing with Argon2id & BCrypt (Băm mật khẩu)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
admin123password123secret2026
⚠️ Lưu trữ MD5('admin123') = 0192023a7bbd73250516f069df18b500. Kẻ tấn công tra cứu bảng tính sẵn (Rainbow Table) tìm ra mật khẩu gốc trong 0.001 giây!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// CỰC KỲ NGUY HIỂM TRONG JAVA: Dùng MessageDigest MD5/SHA-256 thuần
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8));
// Lưu hash hex vào DB -> GPU bẻ khóa trong tích tắc!
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN SPRING SECURITY: Sử dụng Argon2PasswordEncoder
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}
// Lưu mật khẩu:
String hash = passwordEncoder.encode(rawPassword);
// Xác thực đăng nhập:
boolean ok = passwordEncoder.matches(rawPassword, storedHash);
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Tại sao SHA-256 lại KHÔNG được khuyến nghị dùng trực tiếp để băm mật khẩu người dùng?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#03 Multi-Factor Authentication — MFA in Spring Boot
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
MFA giống như chiếc két sắt ngân hàng đòi hỏi 2 loại chìa khóa: một là chiếc chìa khóa sắt bạn cầm trong tay (Thứ bạn CÓ), và hai là mật mã nhớ trong đầu (Thứ bạn BIẾT). Dù tên trộm có móc túi lấy cắp chiếc chìa khóa sắt của bạn, hắn vẫn bất lực vì không biết mật mã trong đầu bạn.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Multi-Factor Authentication — MFA in Spring Boot
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
Đúng Pass + Thiếu OTPPass + Fake OTP 999999Pass + TOTP Hợp Lệ 839210
⚠️ Chỉ dùng Mật khẩu đơn thuần: Nếu mật khẩu bị rò rỉ hoặc lộ qua Phishing, kẻ tấn công chiếm quyền tài khoản ngay lập tức!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THIẾU MFA: Đăng nhập thành công chỉ bằng username & password
@PostMapping("/login")
public ResponseEntity> login(@RequestBody LoginRequest req) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password())
);
SecurityContextHolder.getContext().setAuthentication(auth);
return ResponseEntity.ok("Logged in"); // Cho vào ngay!
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN MFA SPRING BOOT: Xác thực TOTP qua thư viện aerogear-otp-java
import org.jboss.aerogear.security.otp.Totp;
@PostMapping("/verify-totp")
public ResponseEntity> verifyTotp(@RequestBody TotpVerificationRequest req) {
User user = userService.findTempAuthenticatedUser(req.tempToken());
Totp totp = new Totp(user.getMfaSecret());
// Kiểm tra mã 6 số trong cửa sổ thời gian (RFC 6238)
if (!totp.verify(req.code())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Mã TOTP không đúng");
}
String accessToken = jwtService.generateAccessToken(user);
return ResponseEntity.ok(new AuthResponse(accessToken));
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Phương thức xác thực 2 lớp (MFA) nào dưới đây có tính bảo mật cao nhất và chống Phishing triệt để?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#04 Session-based Authentication (Bảo mật Phiên làm việc)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Hãy tưởng tượng bạn vào một khu nghỉ dưỡng cao cấp. Tại quầy lễ tân, họ ghi thông tin của bạn vào cuốn sổ cái (Server Session Store), sau đó đưa cho bạn một chiếc vòng tay chỉ có in mỗi dãy số thẻ phòng (Session ID). Mỗi khi bạn đi ăn buffet hay bơi lội, nhân viên chỉ quét mã vòng tay và tra cứu vào sổ cái. Nếu vòng tay bị ai đó sao chép (Session Hijacking), người đó sẽ xài chùa toàn bộ dịch vụ của bạn.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Session-based Authentication (Bảo mật Phiên làm việc)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
Cookie thiếu HttpOnlySession Fixation AttackMigrateSession chống Fixation
⚠️ Cookie phiên không có cờ HttpOnly: Hacker có thể dùng JavaScript (document.cookie) để đánh cắp JSESSIONID và giả mạo bạn!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// CẤU HÌNH CŨ: Không cấu hình chống Session Fixation
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement(s -> s.sessionFixation().none()); // RẤT NGUY HIỂM!
return http.build();
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN BẢO MẬT SPRING SECURITY 6: Đổi Session ID + Cookie an toàn
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation().migrateSession() // Tự động đổi Session ID mới khi login
.maximumSessions(1) // Giới hạn 1 phiên đồng thời
);
return http.build();
}
// application.properties:
// server.servlet.session.cookie.http-only=true
// server.servlet.session.cookie.secure=true
// server.servlet.session.cookie.same-site=strict
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Cờ 'HttpOnly' trên Cookie phiên giải quyết vấn đề bảo mật nào?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#05 Token-based Authentication (Xác thực Token Bearer)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Thay vì lễ tân phải lật sổ cái để tra cứu mỗi lần bạn đến ăn (Session-based), họ cấp cho bạn một tấm vé điện tử có chữ ký số mã hóa của Giám đốc. Bạn mang tấm vé này đến bất kỳ chi nhánh nào trong hệ thống nhà hàng, bảo vệ chỉ cần dùng khóa công khai để soi chữ ký xem có đúng do Giám đốc ký hay không là cho qua ngay, không cần gọi điện về trụ sở chính (Stateless).
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Token-based Authentication (Xác thực Token Bearer)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
Bearer eyJhbG...Token trong URL ?token=xyzHeader thiếu Authorization
⚠️ Token gửi qua URL Parameter (?token=...): Bị ghi lại trong Browser History, Proxy log, Server log và rò rỉ qua Referer header!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// SAI LẦM: Nhận token từ Query String trên URL trong Spring Controller
@GetMapping("/profile")
public ResponseEntity> getProfile(@RequestParam("token") String token) {
// Rất nguy hiểm: Token lưu vết trong Access log máy chủ web!
return ResponseEntity.ok(jwtService.validate(token));
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN: Lấy token từ Header Authorization Bearer bằng Spring Security Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String jwt = authHeader.substring(7);
// Xác thực token và nạp vào SecurityContextHolder
jwtService.authenticate(jwt, request);
}
filterChain.doFilter(request, response);
}
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Vì sao việc đặt Authentication Token trên URL (Query String) bị coi là một lỗ hổng nghiêm trọng?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#06 JSON Web Token — JWT Security (Bảo mật JWT với JJWT)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
JWT gồm 3 phần: [Header].[Payload].[Signature]. Nó giống hệt như một tấm Séc ngân hàng: Header nói loại séc, Payload ghi 'Chuyển cho Alice 1 tỷ', và Signature là chữ ký tay của ngân hàng. Nếu kẻ gian sửa số tiền thành 100 tỷ trong Payload, chữ ký ở cuối sẽ lập tức không còn khớp với nội dung đã sửa, và ngân hàng sẽ xé tấm séc ngay.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): JSON Web Token — JWT Security (Bảo mật JWT với JJWT)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
{"alg":"none"}{"sub":"admin","role":"admin"}HMAC Secret yếu: 'secret123'
⚠️ Tấn công Alg: None: Kẻ tấn công xóa bỏ chữ ký và gửi token với header alg='none'. Server ngây thơ chấp nhận và nâng quyền lên Admin!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THẢM HỌA: Tự giải mã Base64 không xác thực chữ ký
public Claims parseTokenInsecure(String token) {
String payload = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]));
// Chỉ đọc payload mà KHÔNG verify signature -> Hacker tự chế role Admin!
return objectMapper.readValue(payload, Claims.class);
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN JAVA: Dùng JJWT verifyWith(signingKey)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
public Jws validateAndParseJwt(String token, SecretKey signingKey) {
return Jwts.parser()
.verifyWith(signingKey) // Bắt buộc kiểm tra chữ ký với khóa mật mã
.requireIssuer("https://auth.mycompany.com")
.requireAudience("https://api.mycompany.com")
.build()
.parseSignedClaims(token); // Tự động ném JwtException nếu sai chữ ký hoặc hết hạn!
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Sự khác biệt quan trọng nhất giữa jwt.decode() và jwt.verify() là gì?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#07 Access Token (Token truy cập ngắn hạn)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Access Token giống như chiếc thẻ quẹt thang máy của tòa nhà văn phòng. Thẻ này chỉ có hiệu lực trong vòng 15 phút. Nếu bạn làm rơi thẻ ở sảnh, kẻ nhặt được cũng chỉ dùng được tối đa vài phút trước khi nó trở thành một mẩu nhựa vô dụng. Điều này giảm thiểu tối đa thiệt hại khi bị lộ.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Access Token (Token truy cập ngắn hạn)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
exp: 15m (Chuẩn)exp: 365 days (Quá dài)Không có thời hạn exp
⚠️ Token không có hạn sử dụng (hoặc sống 1 năm): Khi bị rò rỉ, hacker có quyền truy cập vĩnh viễn vào tài khoản của bạn!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Sinh token sống 30 ngày
String token = Jwts.builder()
.subject(userId)
.expiration(new Date(System.currentTimeMillis() + 30L * 24 * 3600 * 1000)) // 30 ngày!
.signWith(key)
.compact();
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Access Token chỉ sống tối đa 15 phút
String accessToken = Jwts.builder()
.subject(user.getId().toString())
.claim("scope", "read:profile")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000)) // 15 phút
.signWith(key, Jwts.SIG.HS256)
.compact();
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Tại sao Access Token nên có thời gian sống (TTL) ngắn (ví dụ: 10 - 15 phút)?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#08 Refresh Token & Rotation in Spring Boot (Làm mới Token)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Nếu Access Token là chiếc vé xem phim 2 tiếng, thì Refresh Token là tấm 'Thẻ hội viên VIP' cất trong két sắt an toàn ở nhà. Bạn không mang thẻ VIP đi quẹt linh tinh ngoài đường. Khi vé xem phim hết giờ, bạn mang thẻ VIP đến quầy để lấy vé mới. Đặc biệt với cơ chế 'Xoay vòng' (Rotation): mỗi khi quầy cấp vé mới, họ cũng hủy luôn thẻ VIP cũ và đổi cho bạn thẻ VIP mới cứng. Nếu kẻ trộm dùng lại thẻ VIP cũ, ngân hàng phát hiện ngay và khóa luôn cả tài khoản.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Refresh Token & Rotation in Spring Boot (Làm mới Token)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
Refresh Token vĩnh viễn (No Rotation)Bật Token Rotation + Reuse DetectionLưu Refresh Token ở localStorage
⚠️ Không có Token Rotation: Khi Refresh Token bị đánh cắp, hacker có thể tự động tạo ra hàng triệu Access Token mới mà nạn nhân không hề hay biết!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THIẾU ROTATION: Cấp lại Access token mà giữ nguyên Refresh Token cũ
@PostMapping("/refresh")
public ResponseEntity> refresh(@RequestBody RefreshRequest req) {
RefreshToken token = tokenRepo.findByToken(req.token()).orElseThrow();
// Giữ nguyên token cũ -> Hacker dùng trộm mãi mãi!
String newAccess = jwtService.createAccessToken(token.getUser());
return ResponseEntity.ok(new AuthResponse(newAccess, req.token()));
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN REFRESH TOKEN ROTATION & REUSE DETECTION TRONG SPRING BOOT:
@Transactional
@PostMapping("/refresh")
public ResponseEntity> refreshToken(@CookieValue("refreshToken") String tokenStr) {
RefreshToken token = tokenRepo.findByToken(tokenStr)
.orElseThrow(() -> new TokenRefreshException("Token không tồn tại"));
// Phát hiện gian lận: Token đã bị thu hồi mà lại đem ra dùng lại!
if (token.isRevoked()) {
tokenRepo.revokeAllTokensForUser(token.getUser().getId()); // HỦY TOÀN BỘ PHIÊN!
throw new SecurityException("Phát hiện xâm nhập trái phép! Vui lòng đăng nhập lại.");
}
// Thu hồi token hiện tại
token.setRevoked(true);
tokenRepo.save(token);
// Cấp phát token mới hoàn toàn
RefreshToken newRefreshToken = tokenService.createRefreshToken(token.getUser());
String newAccessToken = jwtService.createAccessToken(token.getUser());
ResponseCookie cookie = ResponseCookie.from("refreshToken", newRefreshToken.getToken())
.httpOnly(true).secure(true).path("/api/auth").sameSite("Strict").build();
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new TokenResponse(newAccessToken));
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Cơ chế 'Refresh Token Reuse Detection' (Phát hiện dùng lại) hoạt động như thế nào khi có kẻ tấn công?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#09 Account Lockout & Exponential Backoff in Spring Boot (Khóa tài khoản)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Giống như ổ khóa số của vali. Nếu ai đó liên tục bấm sai mã 5 lần, chiếc vali sẽ hú còi và khóa chặt bàn phím trong 15 phút, ngăn không cho kẻ trộm ngồi thử lần lượt từ 0000 đến 9999. Tuy nhiên, nếu khóa vĩnh viễn tài khoản của người khác khi gõ sai, kẻ xấu có thể cố tình gõ sai tài khoản của Giám đốc để Giám đốc không thể đăng nhập làm việc (tấn công DoS người dùng). Do đó cần dùng cơ chế Khóa thông minh (Smart Lockout).
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Account Lockout & Exponential Backoff in Spring Boot (Khóa tài khoản)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
Sai 5 lần (Chưa khóa)Sai 10 lần (Khóa cứng vĩnh viễn)Sai 5 lần (Khóa tạm thời 15p + Báo mail)
⚠️ Chưa có cơ chế khóa: Hacker dùng phần mềm tự động thử 10.000 mật khẩu mỗi phút cho đến khi đăng nhập thành công!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THIẾU KHÓA: Cho phép thử vô tận
@PostMapping("/login")
public ResponseEntity> login(@RequestBody LoginDto dto) {
// Không đếm số lần sai -> Hacker thử hàng triệu lần không bị chặn!
boolean ok = authService.verify(dto);
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN SPRING BOOT: Dùng LoginAttemptService với Guava Cache / Redis
@Service
public class LoginAttemptService {
private final int MAX_ATTEMPT = 5;
private LoadingCache attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.build(new CacheLoader<>() { public Integer load(String key) { return 0; } });
public void loginFailed(String key) {
int attempts = attemptsCache.getUnchecked(key);
attemptsCache.put(key, attempts + 1);
}
public boolean isBlocked(String key) {
return attemptsCache.getUnchecked(key) >= MAX_ATTEMPT;
}
public void loginSucceeded(String key) {
attemptsCache.invalidate(key);
}
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Vì sao khóa tài khoản vĩnh viễn ngay lập tức sau 5 lần gõ sai lại là một thiết kế rủi ro?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#10 Password Reset with Secure Random Tokens in Java
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Đặt lại mật khẩu giống như bạn làm mất vé xe ở trung tâm thương mại. Ban quản lý không thể đưa xe cho bạn chỉ vì bạn đọc đúng biển số. Họ phải yêu cầu bạn xuất trình giấy tờ tùy thân, ký vào biên bản có mã xác nhận độc nhất dùng 1 lần, và mã đó chỉ có giá trị trong 15 phút. Nếu mã này đoán được (dùng số ngẫu nhiên yếu) hoặc không bao giờ hết hạn, kẻ gian sẽ dễ dàng cướp xe của bạn.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Password Reset with Secure Random Tokens in Java
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
Token đoán được (Math.random)Token sống vĩnh viễn (Không hết hạn)Crypto Random Token (SecureRandom 32 bytes)
⚠️ Dùng Math.random() để tạo token: Kẻ tấn công phân tích chuỗi giả ngẫu nhiên (Pseudo-random) và đoán được mã reset của tài khoản Admin!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THẢM HỌA: Dùng java.util.Random sinh token reset
Random rand = new Random(); // Dễ đoán được bằng thuật toán LCG!
int token = 100000 + rand.nextInt(900000);
user.setResetToken(String.valueOf(token));
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN JAVA: Dùng SecureRandom + Băm token trước khi lưu vào CSDL
import java.security.SecureRandom;
public void createPasswordResetToken(String email) {
User user = userRepo.findByEmail(email).orElse(null);
if (user != null) {
byte[] randomBytes = new byte[32];
new SecureRandom().nextBytes(randomBytes);
String rawToken = HexFormat.of().formatHex(randomBytes);
// Chỉ lưu bản băm SHA-256 của token vào database
String tokenHash = Hashing.sha256().hashString(rawToken, StandardCharsets.UTF_8).toString();
user.setResetTokenHash(tokenHash);
user.setResetTokenExpiry(Instant.now().plus(15, ChronoUnit.MINUTES));
userRepo.save(user);
emailService.sendResetEmail(user.getEmail(), rawToken);
}
// Luôn trả về phản hồi chung để chống User Enumeration!
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Tại sao giao diện 'Quên mật khẩu' nên trả về thông báo chung chung: 'Nếu email tồn tại, link đã được gửi' thay vì báo 'Email không tồn tại'?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#11 Brute-force Protection & Rate Limiting in Java (Chống vét cạn)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Hãy tưởng tượng quầy bán vé chỉ có 1 nhân viên phục vụ. Nếu một kẻ đứng trước quầy hỏi liên tục 500 câu mỗi giây: 'Mật khẩu là 1? Mật khẩu là 2?...', nhân viên sẽ kiệt sức hoặc bị lộ mật khẩu. Cơ chế Rate Limiting giống như chiếc cổng xoay tự động: mỗi người chỉ được quẹt thẻ tối đa 5 lần mỗi phút. Quá giới hạn đó, cổng sẽ đóng lại và bắt chờ.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Brute-force Protection & Rate Limiting in Java (Chống vét cạn)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
100 req/s từ 1 IPIP Spoofing qua X-Forwarded-ForRate Limit: 5 req/phút
⚠️ Không có Rate Limit: Server tiếp nhận và xử lý toàn bộ 100 request/giây -> CPU 100%, nguy cơ lộ mật khẩu hoặc sập dịch vụ (DoS)!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// KHÔNG CÓ BỘ LỌC TỐC ĐỘ: Nhận request liên tục
@PostMapping("/login")
public ResponseEntity> login(@RequestBody LoginRequest req) {
// Thực hiện truy vấn DB tốn kém và so sánh bcrypt mà không giới hạn
return ResponseEntity.ok(authService.login(req));
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN SPRING BOOT: Dùng Bucket4j Rate Limiting
@Component
public class LoginRateLimiterFilter extends OncePerRequestFilter {
private final Map cache = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
if ("/api/auth/login".equals(req.getRequestURI())) {
String ip = req.getRemoteAddr();
Bucket bucket = cache.computeIfAbsent(ip, k -> Bucket.builder()
.addLimit(Bandwidth.classic(5, Refill.intervally(5, Duration.ofMinutes(1))))
.build());
if (!bucket.tryConsume(1)) {
res.setStatus(429); // Too Many Requests
res.getWriter().write("Quá nhiều lần thử đăng nhập. Vui lòng thử lại sau 1 phút.");
return;
}
}
chain.doFilter(req, res);
}
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Mã trạng thái HTTP nào là chuẩn mực để thông báo cho client biết họ đã gửi quá số lượng request cho phép?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)