Spring Boot/Module 05 — Tổng kết & cheat sheet
~17 phútSpring Security cơ bảnMiễn phí lượt xem

Module 05 — Tổng kết & cheat sheet

Recap, cheat sheet 1 trang, glossary Spring Security, pitfall tổng hợp, self-assessment outcomes. 1 trang để bookmark khi debug 401/403 bất ngờ, JWT validation fail, CORS preflight bị chặn, hay @PreAuthorize không chạy.

TL;DR: Module 05 đã xây đủ Security Layer production-grade: filter chain 3 tầng intercept request trước DispatcherServlet, Spring Security 6 lambda DSL config SecurityFilterChain, UserDetailsService + BCrypt password hashing với cost 12, JWT stateless auth theo RFC 7519 với HS256/RS256 signing, method security @PreAuthorize AOP proxy + SpEL, và CORS/CSRF config đúng chuẩn cho SPA client. Capstone TaskFlow v3 thêm JWT Bearer token + role-based endpoint protection. Đây là trang bookmark — quay lại khi debug 401 không hiểu filter nào reject, @PreAuthorize annotation không có hiệu lực, hay CORS preflight bị browser chặn.

Đã đi qua những gì

Hành trình bắt đầu từ câu hỏi: vì sao thêm spring-boot-starter-security là mọi endpoint đột ngột trả 401? Bài 01 trả lời bằng kiến trúc filter chain: Tomcat không thấy 15+ filter riêng lẻ — chỉ thấy DelegatingFilterProxy duy nhất, lazy-fetch FilterChainProxy bean khi request đầu tiên đến, rồi route vào SecurityFilterChain config của bạn. Authentication flow: filter extract credential → AuthenticationManagerAuthenticationProvider per token type → UserDetailsService query DB → SecurityContextHolder ThreadLocal per-thread. ExceptionTranslationFilter map: AuthenticationException → 401, AccessDeniedException → 403 — phân biệt này quan trọng khi debug.

Bài 02 bóc DSL config Spring Security 6: WebSecurityConfigurerAdapter bị remove hoàn toàn, thay bằng @Bean SecurityFilterChain. requestMatchers() thay antMatchers() — thông minh hơn, nhận diện AntPath vs MVC pattern. Thứ tự rule quyết định: .anyRequest() catch-all phải cuối, không thì nuốt mọi request trước rule cụ thể. Multiple chain với @Order + securityMatcher() cho nhiều nhóm endpoint khác security policy. Lambda DSL bắt buộc — chain .and() deprecated.

Bài 03 bóc traditional auth: DaoAuthenticationProvider gọi UserDetailsService.loadUserByUsername() query DB, sau đó BCryptPasswordEncoder.matches() verify hash. BCrypt là adaptive function: mỗi hash chứa salt ngẫu nhiên + work factor — tăng cost làm brute force tốn kém hơn. Cost 12 minimum production (~250ms per hash). HTTP Basic gửi base64(user:password) — không phải encryption, chỉ encoding — bắt buộc HTTPS. Password upgrade strategy: re-hash khi user login thành công với DelegatingPasswordEncoder.

Bài 04 là trung tâm của REST API auth 2026: JWT (RFC 7519) format header.payload.signature base64url — payload chứa standard claims (iss, sub, aud, exp, jti), không encrypt, anyone decode được. HS256 symmetric (1 secret cho cả sign + verify) vs RS256 asymmetric (private sign, public verify) — blast radius khác nhau: leak HS256 secret = breach toàn hệ thống, leak RS256 public key = vô hại. Spring Security oauth2ResourceServer.jwt() setup BearerTokenAuthenticationFilter + JwtDecoder tự động. JwtAuthenticationConverter extract custom claim thành GrantedAuthority. Refresh token rotation và jti blocklist cho revocation.

Bài 05 bóc method security tầng thứ 2: @EnableMethodSecurity bật @PreAuthorize / @PostAuthorize. Cơ chế AOP proxy: Spring wrap bean trong CGLIB proxy, intercept method call, invoke AuthorizationManager kiểm tra SpEL expression trước khi delegate. Self-call this.method() bypass proxy — annotation không chạy — pitfall number 1. SpEL cho phép rule động: authentication.name, #methodArg, returnObject. Custom PermissionEvaluator cho ownership check @PreAuthorize("hasPermission(#id, 'project', 'write')"). Defense in depth: URL rule fail-fast + method security backstop.

Bài 06 đóng vòng browser security: Same-Origin Policy (RFC 6454) định nghĩa origin là tuple (scheme, host, port). CORS là opt-in server cho cross-origin đọc response — preflight OPTIONS request gửi trước mọi non-simple request. Forbidden combination: allowCredentials(true) + allowedOrigins("*") vi phạm spec, browser từ chối. CSRF attack lợi dụng cookie auto-attach cross-site — chống bằng Synchronizer Token Pattern, Double-Submit Cookie, hoặc SameSite=Strict. Stateless JWT API: không cần CSRF token vì Bearer header không tự đính kèm bởi browser.

Bài 07 mini-challenge TaskFlow v3: User entity + BCrypt password, UserDetailsService query DB, POST /api/auth/login issue JWT, BearerTokenAuthenticationFilter validate token, @PreAuthorize("hasRole('ADMIN')") bảo vệ write endpoint, anonymous read public endpoint, và CORS config cho SPA frontend.

🗺️ Cheat sheet

Annotation/ConfigKhi nào dùngPitfall thường gặp
@EnableWebSecurityTrên @Configuration class SecurityThiếu annotation → Boot không kích hoạt custom filter chain
SecurityFilterChain beanConfig filter chain Spring Security 6WebSecurityConfigurerAdapter đã removed — không extend được nữa
requestMatchers("/path/**")Match URL cho authorization ruleThứ tự quan trọng: cụ thể trước, .anyRequest() cuối — sai thứ tự = rule bị bỏ qua
.sessionManagement(s -> s.STATELESS)JWT REST API — không tạo sessionQuên set STATELESS → Spring tạo session + JSESSIONID cookie dù dùng JWT
UserDetailsServiceLoad user từ DB cho form login / BasicUsernameNotFoundException thay vì return null — phải throw
BCryptPasswordEncoder(12)Hash password productionCost 4 (default dev) quá yếu — brute force GPU vài giây. Cost 12 minimum
passwordEncoder.matches(raw, hash)Verify password loginSo sánh == với hash string là sai (timing attack + salt mismatch)
oauth2ResourceServer.jwt(...)App validate JWT Bearer tokenQuên config → BearerTokenAuthenticationFilter không được register
JwtDecoder beanCustomize JWT validation (issuer, audience)Default decoder không check aud claim — phải explicit nếu cần
JwtAuthenticationConverterExtract roles từ custom JWT claimSpring mặc định đọc scope claim prefix SCOPE_ — phải override cho roles claim custom
HS256 (HMAC-SHA256)JWT signing đơn giản, single-serviceSecret phải share cho cả sign + verify — microservices dùng RS256 thay
RS256 (RSA-SHA256)JWT signing microservices, IdP tách biệtKey RSA 2048-bit minimum (NIST khuyến nghị 3072-bit đến 2030+)
@EnableMethodSecurityBật @PreAuthorize, @PostAuthorizeThiếu annotation → @PreAuthorize im lặng không chạy, không báo lỗi
@PreAuthorize("hasRole('X')")Check role trước khi method chạySelf-call this.method() bypass AOP proxy → annotation vô hiệu
@PostAuthorize("returnObject.owner == authentication.name")Check sau khi method returnLoad toàn bộ object trước khi check — chậm với large dataset
hasRole("ADMIN")Shortcut kiểm tra roleKhác hasAuthority("ADMIN"): hasRole auto-prefix ROLE_ → check ROLE_ADMIN
@CorsConfiguration / CorsFilterCho phép cross-origin frontend gọi APIallowedOrigins("*") + allowCredentials(true) vi phạm spec — browser từ chối
csrf.disable()REST API stateless JWTPhải đảm bảo thực sự stateless — disable CSRF với session-based auth là security hole
SameSite=Strict cookieDefense CSRF cho browser session appStrict block cross-site request hoàn toàn — có thể break OAuth2 redirect flow
SecurityContextHolder.getContext()Lấy auth trong non-controller codeCross-thread (Async, executor) ThreadLocal không propagate — NPE
@AuthenticationPrincipalInject user trong controller methodType mismatch nếu custom UserDetails implementation không match param type

📖 Glossary module

Thuật ngữĐịnh nghĩa 1 câuNguồn
Authentication (authn)Chứng minh danh tính: "tôi là ai" — username/password, JWT token, certificateBài 01
Authorization (authz)Kiểm tra quyền: "user đã xác thực có được làm hành động X không" — role, permission, custom ruleBài 01
PrincipalĐối tượng đại diện danh tính sau khi xác thực — thường là UserDetails chứa username + authoritiesBài 01
CredentialsBằng chứng xác thực (password, token) — Spring Security xoá khỏi memory sau auth thành côngBài 01
GrantedAuthorityQuyền được cấp cho user — 2 convention: ROLE_X coarse-grained hoặc resource:action fine-grainedBài 01
RBAC (Role-Based Access Control)Phân quyền theo role: user có role gì → được làm gì — phù hợp app có ít hơn 10 roleBài 01
ABAC (Attribute-Based Access Control)Phân quyền theo thuộc tính context (thời gian, IP, tenant, ownership) — mạnh hơn RBAC nhưng phức tạp hơnBài 05
DelegatingFilterProxyBridge từ Tomcat sang Spring bean — lazy-fetch FilterChainProxy khi request đầu đến, giải quyết timing issue Tomcat vs Spring contextBài 01
FilterChainProxySpring bean match URL pattern → chọn đúng SecurityFilterChain để delegateBài 01
SecurityFilterChainSpring bean (config của bạn) chứa list filter cho specific URL patternBài 01, 02
AuthenticationManagerInterface 1 method authenticate() — entry point cho toàn bộ authentication flowBài 01
AuthenticationProviderPluggable strategy: mỗi provider handle 1 token type (DaoAuthenticationProvider cho form, JwtAuthenticationProvider cho Bearer)Bài 01
UserDetailsServiceInterface load UserDetails từ DB theo username — implement để integrate với user store của appBài 01, 03
SecurityContextContainer chứa Authentication object cho request hiện tạiBài 01
ThreadLocalJava mechanism store giá trị per-thread — SecurityContextHolder dùng ThreadLocal để cô lập context giữa các request đồng thờiBài 01
BCryptAdaptive password hashing function với salt ngẫu nhiên + work factor điều chỉnh được — industry standard cho password storageBài 03
SaltChuỗi random thêm vào password trước hash — mỗi user 1 salt khác nhau, chống rainbow table attackBài 03
JWT (JSON Web Token, RFC 7519)Token tự đóng gói format header.payload.signature base64url — mang claims self-contained, server không cần nhớ stateBài 04
JWS (JSON Web Signature, RFC 7515)Chuẩn ký JWT — Compact Serialization là format 3 phần phân tách dấu chấm dùng cho JWT phổ biếnBài 04
JWK (JSON Web Key, RFC 7517)Chuẩn biểu diễn cryptographic key dạng JSONBài 04
JWKS (JWK Set)Endpoint công khai (/.well-known/jwks.json) issuer expose public key — Resource Server fetch để validate token, auto rotateBài 04
ClaimsCặp key-value trong JWT payload — standard: iss, sub, aud, exp, iat, jti, nbfBài 04
Bearer scheme (RFC 6750)Client gửi token qua Authorization: Bearer <token> — "ai cầm token thì có quyền", bắt buộc HTTPSBài 04
OAuth2 (RFC 6749)Framework uỷ quyền — cho service A access resource user trên service B không share password. 4 vai: Resource Owner, Client, Authorization Server, Resource ServerBài 04
Resource ServerApp nhận request có Bearer token, validate signature + expiry, extract claims — cấu hình qua oauth2ResourceServer.jwt() trong SpringBài 04
AOP (Aspect-Oriented Programming)Pattern thêm behavior (security check, log) vào method không sửa code method — Spring implement qua CGLIB proxyBài 05
SpEL (Spring Expression Language)DSL evaluate expression runtime — dùng trong @PreAuthorize để access authentication, method args #param, returnObjectBài 05
Self-call bypassMethod gọi this.method() trong cùng class → bypass AOP proxy → @PreAuthorize/@Transactional không có hiệu lựcBài 05
Same-Origin Policy (RFC 6454)Browser policy: JavaScript chỉ đọc response cùng origin (scheme, host, port) — chặn ăn cắp dữ liệu cross-siteBài 06
CORSCơ chế server opt-in cho cross-origin đọc response qua Access-Control-Allow-Origin header — không phải attack, là policyBài 06
CSRF (Cross-Site Request Forgery)Attack lợi dụng cookie auto-attach cross-site: site độc submit request đến site nạn nhân, browser tự kèm session cookieBài 06
SameSite cookieRFC 6265bis attribute (Strict/Lax/None) chặn cookie gửi cross-site — modern defense CSRF cho session-based authBài 06

⚠️ Pitfall tổng hợp

1. Filter chain order — anyRequest() đặt sai chỗ

// SAI: anyRequest authenticated truoc rule cu the — rule admin bi bo qua
http.authorizeHttpRequests(auth -> auth
    .anyRequest().authenticated()              // catch-all truoc!
    .requestMatchers("/api/admin/**").hasRole("ADMIN")   // NEVER reached
);

// DUNG: cu the truoc, catch-all cuoi
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/auth/**").permitAll()
    .anyRequest().authenticated()              // catch-all cuoi
);

Lý do: Spring duyệt rule từ trên xuống, rule đầu tiên match được áp dụng. anyRequest() match mọi URL — đặt trước rule cụ thể sẽ nuốt tất cả request trước khi đến rule admin.

2. JWT signing key commit lên source control

# SAI: hardcode trong application.properties (bi commit)
app.jwt.secret=mySecretKey123

# DUNG: environment variable
app.jwt.secret=${JWT_SECRET}
# Generate random key: openssl rand -base64 32

Lý do: Signing key trong Git = breach. Bất kỳ ai clone repo đều có thể issue JWT hợp lệ giả mạo bất kỳ user. GitHub secret scanning detect trong vài phút — nhưng token đã issue vẫn valid đến exp. Phải rotate key + force logout toàn bộ user.

3. BCrypt cost factor quá thấp

// SAI: cost 4 (minimum valid) — crack nhanh voi GPU
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(4);
}

// DUNG: cost 12 minimum production
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

Lý do: BCrypt cost 4 hash trong ~1ms. GPU hiện đại crack hàng tỷ hash/giây với cost thấp. Cost 12 ~ 250ms per hash — đủ chậm để brute force không khả thi, đủ nhanh để UX login không bị ảnh hưởng (OWASP khuyến nghị hash phải mất ít nhất 100ms).

4. @PreAuthorize self-call bypass AOP proxy

// SAI: self-call bypass proxy, annotation vo hieu
@Service
public class ProjectService {

    public void importAll(List<Request> reqs) {
        reqs.forEach(req -> this.createProject(req));   // self-call!
    }

    @PreAuthorize("hasRole('ADMIN')")
    public Project createProject(Request req) {
        return projectRepo.save(new Project(req.getName()));
    }
}
// createProject duoc goi truc tiep, khong qua proxy
// bat ky user nao cung tao duoc project

// DUNG option 1: inject self proxy
@Service
public class ProjectService {
    @Autowired
    private ProjectService self;

    public void importAll(List<Request> reqs) {
        reqs.forEach(req -> self.createProject(req));   // qua proxy
    }

    @PreAuthorize("hasRole('ADMIN')")
    public Project createProject(Request req) { ... }
}

// DUNG option 2: tach class
@Service
public class ProjectImportService {
    @Autowired
    private ProjectService projectService;   // inject other bean

    public void importAll(List<Request> reqs) {
        reqs.forEach(req -> projectService.createProject(req));
    }
}

Lý do: AOP proxy wrap object từ bên ngoài. Call this.method() đi thẳng vào object thật, không qua proxy — @PreAuthorize không intercept. Security hole không có error message — annotation im lặng bị bỏ qua.

5. CORS allowCredentials(true) + wildcard origin

// SAI: vi pham spec — browser tu choi
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("*"));         // wildcard
    config.setAllowCredentials(true);               // credentials
    // Fetch spec: forbidden combination!
    ...
}

// DUNG: explicit origin khi can credentials
config.setAllowedOrigins(List.of("https://olhub.org"));
config.setAllowCredentials(true);

Lý do: Fetch spec (kế thừa RFC 6454) nghiêm cấm Access-Control-Allow-Origin: * kết hợp với Access-Control-Allow-Credentials: true. Nếu wildcard được phép với credentials, site độc hại đọc được response có chứa sensitive data của user. Browser enforce: nếu server gửi cả 2, browser reject response với CORS error dù server đã respond.

6. CSRF disable với session-based auth

// SAI: session auth + CSRF disabled = attack vector
http
    .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
    .csrf(csrf -> csrf.disable());    // NGUY HIEM!

// DUNG cho session-based app: enable CSRF
http
    .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
    ...;

// DUNG cho JWT stateless API: disable la OK
http
    .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .csrf(csrf -> csrf.disable());    // OK vi Bearer header khong auto-attach

Lý do: CSRF attack phụ thuộc vào cookie auto-attach. JWT Bearer token trong Authorization header không tự đính kèm bởi browser khi cross-site — không cần CSRF token. Nhưng session cookie (JSESSIONID) tự đính kèm — disable CSRF với session auth = CSRF attack vector mở.

7. SecurityContextHolder cross thread pool

// SAI: ThreadLocal khong propagate vao Async thread
@Async
public CompletableFuture<Void> processAsync() {
    return CompletableFuture.runAsync(() -> {
        // SecurityContextHolder.getContext() = null tren async thread!
        String user = SecurityContextHolder.getContext()
            .getAuthentication().getName();   // NullPointerException
    });
}

// DUNG: capture user truoc, pass vao lambda
@Async
public CompletableFuture<Void> processAsync(
    @AuthenticationPrincipal UserDetails currentUser
) {
    String username = currentUser.getUsername();   // capture tren main thread
    return CompletableFuture.runAsync(() -> {
        processForUser(username);   // pass by param
    });
}

Lý do: SecurityContextHolder mặc định dùng ThreadLocalSecurityContextHolderStrategy — store per-thread. Khi switch sang thread pool (executor, @Async, CompletableFuture), ThreadLocal không được propagate — context null, getAuthentication() trả null → NPE.

8. Password logged hoặc serialized

// SAI: log Authentication object co the include credentials
log.debug("Auth: {}", authentication);   // credentials la trong toString()!

// SAI: serialize UserDetails ra JSON response
@GetMapping("/me")
public UserDetails getCurrentUser(@AuthenticationPrincipal UserDetails user) {
    return user;   // password hash trong response!
}

// DUNG: log chi username, return DTO
log.debug("Auth: user={}", authentication.getName());

@GetMapping("/me")
public UserDto getCurrentUser(@AuthenticationPrincipal UserDetails user) {
    return new UserDto(user.getUsername(), user.getAuthorities());
}

Lý do: UserDetails.getPassword() trả BCrypt hash — dù đã hash nhưng hash trong log = attacker có thể crack offline. Serialize UserDetails thành JSON response expose hash cho client — không bao giờ trả password field (kể cả hashed) ra API.

9. hasRole vs hasAuthority nhầm lẫn

// User co authority "ADMIN" (khong co prefix "ROLE_")
Authentication auth = ...;
auth.getAuthorities();   // [SimpleGrantedAuthority("ADMIN")]

// SAI: hasRole auto-prefix ROLE_
@PreAuthorize("hasRole('ADMIN')")    // check "ROLE_ADMIN" — FAIL
// DUNG: hasAuthority check chinh xac
@PreAuthorize("hasAuthority('ADMIN')")    // check "ADMIN" — PASS

// Nguoc lai: User co "ROLE_ADMIN"
@PreAuthorize("hasRole('ADMIN')")         // check "ROLE_ADMIN" — PASS
@PreAuthorize("hasAuthority('ADMIN')")    // check "ADMIN" — FAIL

Lý do: hasRole("X") tự động prefix ROLE_ → kiểm tra ROLE_X. hasAuthority("X") kiểm tra exact string X. Nhầm convention = security check luôn fail (hoặc luôn pass nếu logic ngược). Dùng 1 convention nhất quán toàn app.

10. JWT alg: none attack

// SAI: accept token voi algorithm "none" (no signature)
// Attacker craft JWT: {"alg":"none"} + any payload + empty signature
// Neu JwtDecoder khong enforce algorithm, token duoc accept!

// DUNG: explicit whitelist algorithm
@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = NimbusJwtDecoder
        .withSecretKey(secretKey)
        .macAlgorithm(MacAlgorithm.HS256)   // enforce HS256 only
        .build();

    // Them validator cho issuer, audience
    decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer("https://olhub.org"));
    return decoder;
}

Lý do: JWT spec (RFC 7519) cho phép alg: none — no signature, anyone craft được. Attacker tạo token với {"alg":"none"} và any payload. Một số thư viện cũ accept none nếu không whitelist algorithm. Spring Security NimbusJwtDecoder enforce algorithm mặc định — nhưng phải explicit để tránh downgrade attack.

11. @PostFilter với large dataset

// SAI: load all, filter in-memory — chậm tuyến tính với N
@PostFilter("filterObject.ownerId == authentication.principal.id")
public List<Project> findAllProjects() {
    return projectRepo.findAll();   // load 100k row, filter 99.9% là throw away
}

// DUNG: filter at DB
public List<Project> findAllProjects(@AuthenticationPrincipal UserDetails user) {
    return projectRepo.findByOwnerId(user.getId());   // 1 SQL với WHERE
}

Lý do: @PostFilter nhận kết quả method, lọc từng phần tử bằng SpEL. Method vẫn load toàn bộ data từ DB — N phần tử vào, M phần tử ra, N-M phần tử bị throw away. Với table lớn = DB query tốn kém + memory overhead. Filter at DB query luôn tốt hơn.

12. SecurityFilterChain không set stateless session + dùng JWT

// SAI: default session created -> JSESSIONID cookie + SecurityContext persist
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

// DUNG: stateless session mandatory cho JWT REST API
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

Lý do: Không set STATELESS → Spring tạo HTTP session, lưu SecurityContext vào session giữa request. Với JWT, mỗi request tự authenticate qua token — không cần session. Session tồn tại thừa: memory leak ở server, JSESSIONID cookie gửi kèm response (CSRF exposure nếu không cấu hình đúng).

✅ Self-assessment outcomes

Tick được hết các ô sau, bạn sẵn sàng Module 06 (Testing strategy). Nếu chưa: re-read bài tương ứng trước khi tiếp tục.

  • Explain cơ chế filter chain của Spring Security: DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain và vai trò của từng layer.
    • Nếu chưa: re-read bài 01 section 3-5 ("FilterChainProxy", "Authentication flow"). Vẽ sơ đồ 3 tầng từ trí nhớ không nhìn tài liệu. Giải thích tại sao cần DelegatingFilterProxy thay vì register trực tiếp 15 filter vào Tomcat.
  • Compare session-based stateful auth vs JWT stateless: trade-off scalability, revocation, CSRF exposure.
    • Nếu chưa: re-read bài 03 section "Session vs JWT"bài 04 section "Trade-off". Viết bảng so sánh 5 dimension: scalability, revocation, CSRF risk, mobile-friendly, server-side memory. Giải thích vì sao REST API 2026 chọn JWT nhưng vẫn cần CSRF config cho browser session app.
  • Implement BCrypt password hashing với cost factor đúng chuẩn production và UserDetailsService query DB.
    • Nếu chưa: re-read bài 03 section 2-4 ("UserDetailsService", "BCrypt", "password lifecycle"). Implement từ scratch: UserDetailsService query DB, BCryptPasswordEncoder(12), register endpoint, verify login với wrong password → 401. Thêm DelegatingPasswordEncoder cho upgrade strategy.
  • Diagnose 401 Unauthorized vs 403 Forbidden: AuthenticationException vs AccessDeniedException trong ExceptionTranslationFilter.
    • Nếu chưa: re-read bài 01 section 8 ("Exception flow") và bài 02 section "exceptionHandling". Tạo 2 test case: 1 request không có token (expect 401) và 1 request có token nhưng thiếu role (expect 403). Bật logging.level.org.springframework.security=DEBUG, trace log xem filter nào throw exception nào.
  • Choose CORS strategy phù hợp: allowedOrigins, allowCredentials, preflight caching cho từng use case SPA/mobile/microservice.
    • Nếu chưa: re-read bài 06 section 1-4 ("Same-Origin Policy", "Preflight", "Spring CORS config"). Test CORS với curl -v -X OPTIONS -H "Origin: https://yourapp.com" -H "Access-Control-Request-Method: POST" http://localhost:8080/api/projects. Verify response header Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Max-Age.
  • Design JWT authentication flow: issue token, validate signature, extract claims, refresh token rotation, revocation strategy.
    • Nếu chưa: re-read bài 04 section 3-7 ("JWT issue", "oauth2ResourceServer", "refresh token"). Implement đầy đủ: POST /api/auth/login issue JWT (HS256, exp 15 min), GET /api/me validate token, refresh endpoint rotate access + refresh token. Test với expired token (expect 401), tampered token (expect 401).

🚀 What's next — Module 06: Testing strategy

Module 05 đã có security layer đầy đủ: filter chain, JWT auth, method security, CORS/CSRF. Module 06 thêm testing strategy: unit test với @WebMvcTest + @WithMockUser, integration test với @SpringBootTest + Testcontainers, test security config (ensure endpoint trả đúng 401/403), test @PreAuthorize self-call scenario, và test JWT issue + validate flow.

→ Đi tới Module 06: Testing strategy

📚 Tài liệu mở rộng

Spring Security:

RFC / Spec chính thức:

Security reference:

Sách:

  • Spring Security in Action — Laurentiu Spilca (Manning, 2024 ed.). Cover Spring Security 6 từ đầu — filter chain, OAuth2, method security, test. Cuốn tham khảo toàn diện nhất cho Spring Security 6.
  • OAuth 2.0 Simplified — Aaron Parecki (2020). Giải thích OAuth2 grant types bằng ngôn ngữ đơn giản, nhiều diagram. Đọc trước khi dùng Keycloak/Auth0.

Blog / Tool:

  • Baeldung — Spring Security — 200+ bài tutorial từng feature riêng lẻ.
  • jwt.io — decode/verify JWT trực tuyến. Dùng để debug claims, verify signature.
  • OWASP ZAP — web application security scanner. Test CORS, CSRF, auth bypass tự động.

Chúc mừng — bạn đã hoàn thành Module 05. Security layer đã sẵn sàng: filter chain hiểu được, JWT issue + validate, BCrypt password hash đúng cost, method security không self-call bypass, CORS/CSRF config production-grade. Nghỉ 1 ngày cho các concept lắng xuống — đặc biệt JWT signing và AOP proxy self-call. Rồi vào Module 06 — test toàn bộ security layer vừa xây.

Bài này có giúp bạn hiểu bản chất không?