SecurityFilterChain DSL — khai báo security bằng lambda Spring Security 6
Spring Security 6 thay WebSecurityConfigurerAdapter bằng @Bean SecurityFilterChain + HttpSecurity lambda DSL. Bài này bóc cú pháp requestMatchers, authorizeHttpRequests, các DSL method phổ biến (csrf/cors/sessionManagement/oauth2), và lý do thiết kế: tại sao lambda DSL composable + type-safe hơn chained .and() cũ.
TL;DR: Spring Security 6 loại bỏ WebSecurityConfigurerAdapter — config security nay là một @Bean trả về SecurityFilterChain, khai báo bằng chuỗi lambda http.method(customizer -> ...) rồi return http.build(). requestMatchers() thay antMatchers() cũ, tự nhận diện Ant vs MVC pattern. Thứ tự rule quyết định: specific trước, .anyRequest() catch-all luôn đặt cuối. Các DSL method phổ biến — csrf, cors, sessionManagement, oauth2ResourceServer, exceptionHandling — mỗi thứ là một lambda block tách biệt, type-safe, dễ tái dùng.
Bài trước (Filter chain architecture) giải thích FilterChainProxy dispatch request đến đúng chain. Bài này bóc cú pháp DSL để khai báo chain đó từ scratch. Bài sau (Multiple chains & stateless) đào sâu @Order + securityMatcher cho nhiều chain song song.
1. Tại sao bỏ WebSecurityConfigurerAdapter
Trước Spring Security 6, cách duy nhất để config là kế thừa WebSecurityConfigurerAdapter rồi override method configure(HttpSecurity):
// Spring Security 5 — DEPRECATED, removed in 6
@Configuration
public class OldConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Hai vấn đề cốt lõi của pattern này:
Vấn đề 1 — không composable. Toàn bộ config phải nằm trong một class và một method. Không thể tách "phần auth" và "phần session" vào các @Component riêng rồi ghép lại — override method là all-or-nothing.
Vấn đề 2 — .and() mơ hồ về scope. Mỗi lần gọi .csrf(), .authorizeRequests(), .sessionManagement() đều trả về một sub-configurer khác kiểu. .and() thoát sub-configurer để tiếp tục chain. IDE không thể kiểm tra compile-time bạn đang .and() từ đúng cấp độ — lỗi sai cấu trúc chỉ lộ ra lúc chạy.
Spring Security 6 giải quyết cả hai bằng lambda DSL: mỗi method trên HttpSecurity nhận một Customizer<T> (functional interface), lambda được typed chính xác về T, và config trả thành @Bean thuần — composable, test được, không inheritance.
flowchart LR
subgraph OLD["Spring Security 5 (removed)"]
A["extends WebSecurityConfigurerAdapter"] --> B["override configure(HttpSecurity)"]
B --> C[".csrf().disable().and()<br/>.authorizeRequests()..."]
end
subgraph NEW["Spring Security 6"]
D["@Bean SecurityFilterChain"] --> E["HttpSecurity lambda DSL"]
E --> F["http.csrf(csrf -> csrf.disable())<br/>http.authorizeHttpRequests(auth -> ...)"]
end2. Cấu trúc @Bean SecurityFilterChain
Skeleton tối thiểu của một security config Spring Security 6:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable());
return http.build(); // always required
}
}
Ba điểm cốt yếu:
- Method nhận
HttpSecurity httpqua Spring DI — framework inject sẵn. - Mỗi tính năng security là một lambda block riêng:
auth -> ...,s -> ...,csrf -> .... Lambda parameter là sub-configurer đúng kiểu — IDE autocomplete chính xác, compiler bắt lỗi sai method. return http.build()bắt buộc — trả vềSecurityFilterChainbean. Quên dòng này sẽ compile được nhưng chain không được đăng ký, app không có security.
Method return type là SecurityFilterChain nhưng nếu bạn viết return null hoặc thiếu return, Spring không ném lỗi ngay — nó chỉ không đăng ký chain, toàn bộ request đi qua không bị filter. Luôn kết thúc bằng return http.build().
Luồng request qua chain:
flowchart LR Req["HTTP Request"] Filters["Filters trong chain<br/>(CSRF, Session, Auth...)"] AHR["authorizeHttpRequests<br/>rule check"] Allow["chain.doFilter<br/>Controller xu ly"] Deny401["401 Unauthorized<br/>chua xac thuc"] Deny403["403 Forbidden<br/>khong co quyen"] Req --> Filters Filters --> AHR AHR -->|"permitAll / pass"| Allow AHR -->|"chua xac thuc"| Deny401 AHR -->|"da xac thuc nhung sai role"| Deny403
3. requestMatchers + authorize rules
requestMatchers() thay thế antMatchers() cũ từ Spring Security 5. Phiên bản mới tự nhận diện URL pattern: nếu app dùng Spring MVC, matcher sẽ dùng PathPatternParser của MVC (chính xác hơn); nếu không có MVC, fallback sang Ant-style matching. Không cần khai báo riêng loại matcher.
http.authorizeHttpRequests(auth -> auth
// Path pattern — Ant-style, wildcard ** = zero or more segments
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/auth/**", "/actuator/health").permitAll()
// Method-specific: POST /api/projects chi admin moi tao
.requestMatchers(HttpMethod.POST, "/api/projects").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/projects/**").authenticated()
// Role-based rules
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER")
// Custom authority (khong prefix ROLE_)
.requestMatchers("/api/reports").hasAuthority("reports:read")
// Catch-all -- LUON dat cuoi
.anyRequest().authenticated()
);
Thứ tự rule là thứ tự kiểm tra — first match wins. Spring duyệt từ trên xuống, rule đầu tiên khớp URL được áp dụng, các rule sau bị bỏ qua. Hệ quả: .anyRequest() phải luôn là rule cuối cùng, nếu đặt trước nó sẽ nuốt mọi request và các rule cụ thể phía sau không bao giờ được kiểm tra.
Bảng đầy đủ các authorization expression:
| Expression | Ý nghĩa |
|---|---|
permitAll() | Cho phép tất cả, kể cả anonymous |
denyAll() | Từ chối tất cả |
authenticated() | Bất kỳ user đã xác thực |
anonymous() | Chỉ anonymous (chưa login) |
fullyAuthenticated() | Đã xác thực, không phải remember-me |
hasRole("ADMIN") | Có authority ROLE_ADMIN |
hasAnyRole("USER", "ADMIN") | Có ít nhất một trong các role |
hasAuthority("orders:write") | Có đúng authority này (không prefix ROLE_) |
access(AuthorizationManager) | Logic tùy biến qua custom AuthorizationManager |
hasRole("ADMIN") tự động thêm prefix ROLE_ — nó kiểm tra authority ROLE_ADMIN. hasAuthority("ROLE_ADMIN") và hasRole("ADMIN") tương đương. Nếu authority không có prefix ROLE_ (ví dụ orders:write), phải dùng hasAuthority() thay vì hasRole().
4. Các DSL method phổ biến
HttpSecurity là object trung tâm — mỗi tính năng security gắn vào nó qua một lambda block độc lập:
flowchart TB H["HttpSecurity http"] A["authorizeHttpRequests<br/>requestMatchers rules"] CR["csrf<br/>disable hoac CookieCsrfTokenRepository"] CO["cors<br/>CorsConfigurationSource"] SM["sessionManagement<br/>STATELESS / IF_REQUIRED / NEVER"] EH["exceptionHandling<br/>authEntryPoint / accessDeniedHandler"] RS["oauth2ResourceServer<br/>jwt / jwtAuthenticationConverter"] HD["headers<br/>CSP / HSTS / frameOptions"] BLD["http.build<br/>SecurityFilterChain bean"] H --> A H --> CR H --> CO H --> SM H --> EH H --> RS H --> HD H --> BLD
4.1 CSRF
CSRF (Cross-Site Request Forgery) là tấn công dùng browser tự động gửi cookie session — kẻ tấn công dụ người dùng click link, browser đính kèm session cookie, server không phân biệt được. Spring Security bật CSRF protection mặc định.
// REST API stateless dung JWT -> disable CSRF
// JWT trong Authorization header, browser khong tu dinh kem -> khong co CSRF surface
http.csrf(csrf -> csrf.disable());
// Web app form-based -> giu CSRF, tinh chinh token repo
http.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/webhooks/**") // webhook extern khong co CSRF
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
Nguyên tắc chọn: stateless API dùng JWT trong Authorization header thì disable CSRF — browser không tự đính kèm header cross-site nên không có CSRF surface. Web app dùng session cookie thì giữ CSRF vì cookie tự đính kèm.
4.2 Session management
http.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // REST API + JWT
// .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // default, web app
);
STATELESS là cấu hình bắt buộc cho REST API dùng JWT — Spring Security không tạo và không đọc HTTP session. Auth được rebuild từ token mỗi request. Bài Multiple chains & stateless đào sâu trade-off của từng policy.
4.3 OAuth2 Resource Server (JWT)
Khi app nhận Bearer token từ client và cần validate:
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedConverter = new JwtGrantedAuthoritiesConverter();
grantedConverter.setAuthoritiesClaimName("roles"); // claim chua roles trong token
grantedConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(grantedConverter);
return converter;
}
BearerTokenAuthenticationFilter parse JWT mỗi request, verify signature + expiry, rồi build JwtAuthenticationToken đặt vào SecurityContext. Không cần session.
4.4 Exception handling
Mặc định Spring Security redirect 401 tới /login (trả HTML). REST API cần JSON:
http.exceptionHandling(eh -> eh
.authenticationEntryPoint((req, res, ex) -> {
// 401 -- unauthenticated
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType("application/problem+json");
res.getWriter().write("""
{"type":"about:blank","title":"Unauthorized","status":401}
""");
})
.accessDeniedHandler((req, res, ex) -> {
// 403 -- authenticated but wrong role
res.setStatus(HttpStatus.FORBIDDEN.value());
res.setContentType("application/problem+json");
res.getWriter().write("""
{"type":"about:blank","title":"Forbidden","status":403}
""");
})
);
authenticationEntryPoint xử lý 401 (chưa xác thực). accessDeniedHandler xử lý 403 (đã xác thực nhưng thiếu quyền). Cả hai cần custom cho REST API để frontend SPA nhận JSON thay vì redirect HTML.
4.5 CORS
CORS (Cross-Origin Resource Sharing) là cơ chế browser kiểm tra xem origin của frontend (https://app.example.com) có được phép gọi API (https://api.example.com) không. Spring Security tích hợp với Spring MVC CORS config qua CorsConfigurationSource:
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://olhub.org"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
5. Tại sao lambda DSL — cơ chế composable
Lambda DSL không chỉ là syntax mới. Nó thay đổi cơ bản cách config được tổ chức.
Composable qua Customizer bean. Mỗi lambda block có thể trích thành Customizer<T> bean riêng — inject, test, tái dùng qua nhiều chain:
// Trich thanh bean rieng -- test duoc, tai dung duoc
@Component
public class StatelessSessionCustomizer
implements Customizer<SessionManagementConfigurer<HttpSecurity>> {
@Override
public void customize(SessionManagementConfigurer<HttpSecurity> s) {
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
// Inject va dung trong nhieu chain
@Bean
public SecurityFilterChain apiChain(HttpSecurity http,
StatelessSessionCustomizer sessionCustomizer) throws Exception {
http
.sessionManagement(sessionCustomizer)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
WebSecurityConfigurerAdapter không cho phép điều này — config bị lock trong một class override duy nhất.
Type-safe. Lambda parameter csrf -> ... có kiểu CsrfConfigurer<HttpSecurity>. IDE biết chính xác method nào hợp lệ. .and() style cũ trả về HttpSecurity sau mỗi sub-configurer — compiler không phân biệt được bạn đang gọi method của HttpSecurity hay đang trong một sub-configurer chưa thoát.
Customizer.withDefaults() — default config không cần lambda:
http.httpBasic(Customizer.withDefaults()); // apply Spring default Basic auth config
http.formLogin(Customizer.withDefaults()); // apply Spring default form login
Thay vì http.httpBasic() rỗng (deprecated), withDefaults() rõ ràng về ý định: "dùng config mặc định của Spring cho feature này".
6. Pitfall thường gặp
Pitfall 1 — .anyRequest() không phải rule cuối:
// SAI -- anyRequest nuot tat ca request truoc khi rule cu the chay
http.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN") // never reached
);
// DUNG -- cu the truoc, catch-all sau
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
Pitfall 2 — WebSecurityConfigurerAdapter vẫn còn trong classpath:
Spring Security 6 đã xóa class này. Nếu bạn thấy lỗi Cannot resolve symbol 'WebSecurityConfigurerAdapter' sau khi upgrade, đây là tín hiệu cần migrate sang @Bean SecurityFilterChain. OpenRewrite có recipe tự động hóa phần lớn việc này: org.openrewrite.java.spring.security6.RemoveFilterSecurityInterceptorOncePerRequest.
Pitfall 3 — nhiều chain không có securityMatcher, chain đầu nuốt tất cả:
// SAI -- chain 1 khong co securityMatcher -> match moi request -> chain 2 khong chay
@Bean @Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN"));
return http.build();
}
@Bean @Order(2)
public SecurityFilterChain publicChain(HttpSecurity http) throws Exception { // never used
http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
// DUNG -- chain 1 co securityMatcher de gioi han pham vi
@Bean @Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/admin/**") // chi xu ly URL nay
.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN"));
return http.build();
}
Bài Multiple chains & stateless đào sâu pattern nhiều chain với @Order + securityMatcher.
Pitfall 4 — hasRole nhầm prefix:
// SAI -- authority trong DB la "ROLE_ADMIN", hasRole tu them prefix -> kiem tra "ROLE_ROLE_ADMIN"
.requestMatchers("/api/admin/**").hasRole("ROLE_ADMIN")
// DUNG -- hasRole tu them ROLE_ prefix, chi viet ten role
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Tuong duong voi:
.requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
Liên hệ các bài khác
- Bài 01 — Filter chain architecture:
FilterChainProxydispatch request đến đúngSecurityFilterChaintheo thứ tự — bài này giải thích runtime bên dưới; DSL ở bài hiện tại khai báo chain đó ra sao. - Bài 04 — Multiple chains & stateless: đào sâu
@Order+securityMatcherkhi app cần nhiều chain song song (admin JWT vs public permitAll vs web session), và cơ chếSessionCreationPolicy.STATELESSvới JWT. WebSecurityConfigurerAdapterbị xóa là một phần của Spring Security 6 migration — xem Spring Security Migration Guide để hiểu toàn bộ breaking changes, không chỉ riêng DSL.
Tóm tắt
- Spring Security 6 xóa
WebSecurityConfigurerAdapter— config bằng@Bean SecurityFilterChain, nhậnHttpSecurity, returnhttp.build(). - Lambda DSL: mỗi tính năng là một lambda block
http.feature(x -> x.setting(...))— type-safe, composable thànhCustomizerbean riêng. requestMatchers()thayantMatchers(): tự nhận diện Ant vs MVC pattern, hỗ trợHttpMethod-specific.- Thứ tự rule — first match wins: specific trước,
.anyRequest()catch-all luôn cuối. - Các DSL method thường dùng:
csrf,cors,sessionManagement,oauth2ResourceServer,exceptionHandling,headers. - Stateless REST API:
csrf.disable()+sessionCreationPolicy(STATELESS)+oauth2ResourceServer.jwt(...)+ customexceptionHandlingtrả JSON.
Tự kiểm tra
Q1Spring Security 6 bỏ WebSecurityConfigurerAdapter vì lý do gì? Nêu 2 vấn đề của pattern extends-override cũ.▸
WebSecurityConfigurerAdapter vì lý do gì? Nêu 2 vấn đề của pattern extends-override cũ.Vấn đề 1 — không composable: config phải nằm toàn bộ trong một class override một method. Không thể tách các phần config (auth rule, session policy, CORS) thành các @Component riêng rồi tái dùng qua nhiều chain. Adapter pattern là all-or-nothing — muốn thay đổi một phần nhỏ vẫn phải override toàn bộ.
Vấn đề 2 — .and() mơ hồ về scope: mỗi sub-configurer (csrf(), authorizeRequests(), sessionManagement()) trả về kiểu khác nhau. .and() thoát sub-configurer để tiếp tục chain, nhưng compiler không kiểm tra bạn đang .and() từ đúng cấp độ hay không — lỗi sai cấu trúc chỉ lộ ra lúc runtime.
Lambda DSL giải quyết cả hai: mỗi lambda block scope rõ ràng (tham số typed chính xác, IDE autocomplete đúng), và có thể trích thành Customizer<T> bean để inject + tái dùng qua nhiều chain.
Q2Config sau có lỗi gì? Endpoint nào bị broken và tại sao?
http.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
)▸
http.authorizeHttpRequests(auth -> auth .anyRequest().authenticated() .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN"))Lỗi: anyRequest() đứng trước các rule cụ thể. Spring duyệt rule theo thứ tự từ trên xuống và dùng rule đầu tiên khớp — first match wins. anyRequest() match mọi URL, nên nó được áp dụng cho toàn bộ request trước khi đến /api/public/** hay /api/admin/**.
Hệ quả:
/api/public/**bị broken: rulepermitAll()không bao giờ được kiểm tra, endpoint này yêu cầu auth thay vì public./api/admin/**bị broken: rulehasRole("ADMIN")không chạy, bất kỳ user đã xác thực nào cũng vào được.
Fix — specific trước, catch-all cuối:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // catch-all LUON cuoi
)Q3hasRole("ADMIN") và hasAuthority("ROLE_ADMIN") có tương đương không? Khi nào dùng hasAuthority() thay vì hasRole()?▸
hasRole("ADMIN") và hasAuthority("ROLE_ADMIN") có tương đương không? Khi nào dùng hasAuthority() thay vì hasRole()?Tương đương: hasRole("ADMIN") tự động thêm prefix ROLE_ trước khi kiểm tra, tức nó kiểm tra authority ROLE_ADMIN. hasAuthority("ROLE_ADMIN") kiểm tra đúng chuỗi đó. Hai cách viết cho kết quả như nhau khi authority có prefix ROLE_.
Khi dùng hasAuthority(): khi authority không có prefix ROLE_, ví dụ fine-grained permission như orders:write, reports:read, users:delete. Đây là pattern ABAC (attribute-based) thay vì RBAC thuần. Nếu dùng hasRole("orders:write"), Spring sẽ kiểm tra ROLE_orders:write — không match.
Pitfall phổ biến: authority trong DB là ROLE_ADMIN, nhưng config viết hasRole("ROLE_ADMIN") → Spring kiểm tra ROLE_ROLE_ADMIN → không bao giờ match → mọi request đến admin endpoint đều bị 403 mà không rõ lý do.
Q4Vì sao REST API stateless dùng JWT nên gọi csrf(csrf -> csrf.disable())? Giải thích theo cơ chế tấn công CSRF.▸
csrf(csrf -> csrf.disable())? Giải thích theo cơ chế tấn công CSRF.Cơ chế CSRF: kẻ tấn công dụ người dùng truy cập trang độc hại. Trang đó chứa form hoặc request tới API của bạn. Trình duyệt tự động đính kèm cookie (session cookie) khi gửi request — kể cả khi request xuất phát từ domain khác. Server nhận session cookie hợp lệ, không phân biệt được request từ app thật hay từ trang độc hại.
Tại sao JWT không có CSRF surface: JWT được lưu trong localStorage hoặc memory của JS, không phải cookie. Client phải chủ động đính kèm vào Authorization: Bearer ... header. Trình duyệt không tự động thêm header tùy ý cross-origin — CORS policy của browser chặn điều đó. Kẻ tấn công không thể khiến browser tự gửi JWT từ trang độc hại.
Hệ quả: với stateless JWT API, không có session cookie để lợi dụng → không có CSRF surface → disable CSRF protection là hợp lý và giảm overhead không cần thiết. Nếu app vẫn dùng session cookie song song JWT, vẫn cần bật CSRF.
Q5Viết skeleton config cho REST API stateless với JWT: cho phép /api/auth/** public, /api/admin/** chỉ ADMIN, còn lại cần xác thực; trả JSON cho 401/403.▸
/api/auth/** public, /api/admin/** chỉ ADMIN, còn lại cần xác thực; trả JSON cho 401/403.@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Stateless REST API -- khong dung session, JWT tu Authorization header
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// REST API stateless -- JWT in header, khong co CSRF surface
.csrf(csrf -> csrf.disable())
// Authorization rules -- specific truoc, catch-all cuoi
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// Validate JWT Bearer token moi request
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
// Tra JSON thay vi redirect HTML
.exceptionHandling(eh -> eh
.authenticationEntryPoint((req, res, ex) -> {
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType("application/problem+json");
res.getWriter().write(
"{"status":401,"title":"Unauthorized"}");
})
.accessDeniedHandler((req, res, ex) -> {
res.setStatus(HttpStatus.FORBIDDEN.value());
res.setContentType("application/problem+json");
res.getWriter().write(
"{"status":403,"title":"Forbidden"}");
})
);
return http.build(); // bat buoc
}
}Bốn điểm cần nhớ: (1) STATELESS để không tạo session. (2) csrf.disable() vì JWT không có CSRF surface. (3) thứ tự rule: public → role-based → catch-all. (4) custom exceptionHandling để frontend nhận JSON, không HTML redirect.
Bài tiếp theo: Multiple chains & stateless config
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên