Part 2: Cấu hình CORS trong Spring Boot ở các cấp Controller, Global và trong Spring Security và lý do tại sao?
Chào mọi người, Để tiếp nối Part 1 mình đã giới thiệu mọi người tất tần tật về CORS rồi, thì trong phần này mình sẽ tiến hành cấu hình nó trong Spring Boot nhé.
Bắt đầu thôi nào
Trong bài này thì mình sẽ giới thiệu mọi người các cách cấu hình cors trong Spring Boot, thì bạn có 3 cấp độ chính để cấu hình CORS, ngoài ra nếu bạn dùng Spring Webflux thì bạn cũng có cách cấu hình với Spring Webflux nhưng trong phạm vi bài viết này mình không đề cập đến:
Vậy thì trong Spring Boot bạn có cách chính:
- Mức độ
Controller
- Cấu hình CORS ở mức
Global (WebMvcConfigurer)
- Cấu hình trong
Spring Security
1. Cấu hình trong Controller
Đây là cách đơn giản nhất để bật CORS cho một API cụ thể. Cần sử dụng annotation @CorssOrigin ngay trên controller hoặc trên từng endpoit.
1.1 Ưu điểm:
- Phù hợp khi bạn chỉ muốn cấu hình CORS cho một số endpoint cụ thể.
- Rất dễ hiểu và dễ quản lý nếu bạn có ít endpoint.
@RestController
@RequestMapping("/api/users")
public class UserController {
// Bật CORS chỉ cho endpoint này
@CrossOrigin(origins = "http://example.com")
@GetMapping("/{id}")
public String getUser(@PathVariable String id) {
return "User ID: " + id;
}
// Không có @CrossOrigin => Không bật CORS
@PostMapping
public String createUser(@RequestBody String user) {
return "User created: " + user;
}
}
- Ở ví dụ trên, trình duyệt chỉ cho phép các request từ http://example.com khi gọi API /api/users/{id}.
- API /api/users không có cấu hình CORS, nên nếu gọi từ domain khác, trình duyệt sẽ chặn request.
1.2 Nhược điểm của cấu hình Cors ở Controller:
1. Khóa quản lý đối với ứng dụng lớn:
- Nếu có nhiều controller hoặc endpoit thì việc sử dụng @CrossOrigin trên từng controller/endpoint sẽ dẫn đến cấu hình lặp lại, gây khó khăn khi bảo trì.
- Ví dụ: Nếu chính sách CORS thay đổi, bạn phải chỉnh sửa ở rất nhiều nơi, dễ dẫn đến lỗi hoặc cấu hình không đồng nhất.
- Chỉ những endpoit có gắn @CrossOrigin mới được cấu hình, còn lại thì không.
2. Không phù hợp cho các cấu hình CORS phức tạp:
- @CrossOrigin không cung cấp tất cả các tùy chọn phức tạp như quản lý
headers được phép
, caching thời gian preflight, hay việc bật/tắtcredentials
. - Để cấu hình chi tiết hơn, bạn cần sử dụng cấu hình
global
hoặcSpring Security
.
3. Vấn đề lớn nhất ở đây là ứng dụng bạn có Spring Security
:
Khi ứng dụng Spring Boot của bạn có xài Spring Security
, thì các request HTTP được xử lý bởi filter chain
của Spring Security
trước khi đến controller
. Đây thì nó xảy ra các vấn đề sau đây nè:
- Ví dụ bạn có 1
preflight request
như mình có đề cập ở part 1, thì khi request này sẽ bị reject bởiSpring Security filter chain
- Trong Spring Security không bật http.cors() thì mọi cấu hình cors ở controller sẽ bị bỏ qua.
Nghe có vẻ khó hiểu nhỉ, thôi để tui đưa một số code demo cho nè!
1.3 Demo cấu hình cors ở controller:
Khi không cấu hình cors:
//@CrossOrigin("*")
@RestController
@RequestMapping("/api")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/users")
public List<String> getUsers(Authentication authentication) {
return List.of("User1", "User2", "User3");
}
}
Anh em sẽ nhận được 1 lỗi tương tự như sau:
Note: Anh em đừng để ý mấy cái header nào là
Authorization
này kia nha, demo code thừa thôi 😂
Dùng CrossOrigin
Để Fix lỗi trên thì dễ dàng thôi 😂.
Bật cái dòng comment lên là được, ở đây demo nên tui dùng *
nhé, trong production thì khuyến cáo, khuyến cáo là không được dùng như thế nhé, như thế thì origin
nào cũng có thể gọi đến APIs của bạn đó 😳
@CrossOrigin("*")
@RestController
@RequestMapping("/api")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/users")
public List<String> getUsers(Authentication authentication) {
return List.of("User1", "User2", "User3");
}
}
Quá dễ dàng, mọi thứ hoạt động hoàn hảo khi không có Spring Security
1.4 Vấn đề sử dụng @CrossOrigin khi có Spring Security:
Khi trong ứng dụng của anh em vô tình cóp dán ở đâu đó hoặc nó có sẵn Spring Security rồi, ví dụ trong build.gradle
như thế này.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
}
Note: là khi thêm
Spring Security
thì mặc định là nó bậtauthorized
mọi endpoint nhé, anh em nào mới học mà tới bước nào vọc phá thêm vô rồi không hiểu sao mà nó bị401
hết thì lưu ý nhé 🥲
Không làm gì cả mà nó đã tự động làm giúp mình rồi:
http
.authorizeHttpRequests((authz) -> authz.anyRequest().authenticated())
.httpBasic()
Demo reuqest Options
bị chặn bởi Spring Security
Ở đây mình dùng DemoController
khác để test thử khi dùng Spring security
nhé, mình sẽ demo cho anh em thấy là các request GET, POST thì thực hiện bình thường nhưng đến khi OPTIONS
thì bị Spring Security
nó chặn lại nhé:
Đây là controller của mình nhé DemoController.java
@CrossOrigin(origins = "https://viblo.asia") // Allow requests from this origin
@RestController
@RequestMapping("/api")
public class DemoController {
@GetMapping("/public-data")
public String publicData() {
return "This is public data";
}
@GetMapping("/private-data")
public String privateData(Authentication authentication) {
return "This is private data for: " + authentication.getName();
}
}
Cấu hình Spring Security
của mình ở SecurityConfig.java
:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/private-data").authenticated() // Only authenticated users can access
.anyRequest().permitAll() // Allow access to everything else
)
.httpBasic(Customizer.withDefaults()) // Enable HTTP Basic for authentication
.csrf(csrf -> csrf.disable()); // Disable CSRF for simplicity in the demo
return http.build();
}
}
1. Thực hiện GET
request:
Đây payload request mẫu anh em có thể cóp dán vô console để test nhé, anh em nhớ đứng ở violo.asia
nhé, nhớ đỗi pass lại thành pass mà Spring Boot nó sinh ra ở output
nhé, vì mình đang dùng Basic Authen
:
fetch("http://localhost:8080/api/private-data", {
method: "GET",
headers: {
Authorization: "Basic " + btoa("user:0720d3aa-5a1b-4c70-b18b-40eb12535623"), // Replace with actual credentials
},
})
.then((response) => response.text())
.then((data) => console.log(data))
.catch((err) => console.error(err));
Anh em có thể thấy không hề có một lỗi CORS
nào quăng ra rất chi là bình thường.
2. Thực hiện OPTIONS
request:
Đến OPTIONS
thì anh em có thể thấy là dính lỗi CORS
rồi, cay thiệt chứ 😂, vì request nó đi qua security filterchain
trước khi đi đến controller nên không có config cho OPTIONS
thì nó chặn lại.
Vậy thì làm sao fix đây, anh em phải config cho phép OPTIONS
request ở chổ security filterchain
thôi.Anh em bắt đầu thấy nó bị lủng củng chưa, vậy thì config ở controller để làm gì vừa mất thời gian, vừa cồng kềnh lại dính thêm cái bug khi dùng với Spring Security
nữa.
Ở đây tui sẽ không cung cấp code để pass lỗi này ở đây, vì tui sẽ cung cấp nó ở phần Cấu hình trong Spring Security
nhé.
Tóm lại khi nào cấu hình cors ở controller:
- Khi anh em dùng cho các ứng dụng đơn giản.
- Điều quan trọng là anh em không dùng với
Spring Security
khi có các request không phải làSimple request
(phần này tui có giải thích ở part 1) thì có thể tạm chấp nhận.
2. Cấu hình CORS ở mức Global (WebMvcConfigurer)
Mình sẽ demo cấu hình CORS ở mức Global sử dụng WebMvcConfigurer
nhé.
Cấu hình cors nằm ở GlobalCorsConfig.java
:
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Apply CORS policy to all endpoints
.allowedOrigins("https://example.com") // Allowed origins
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // HTTP methods
.allowedHeaders("Authorization", "Content-Type") // Allowed headers
.allowCredentials(true); // Allow credentials (cookies, Authorization headers, etc.)
}
}
Mình vẫn sử dụng DemoController
kia để test nhé, anh em có thể thấy allowedOrigins
của mình không có cấu hình domain của viblo
nên khi thực hiện request thì vẫn dính CORS nhé:
Note: Trong phần này anh em không thấy tui gữi thêm user:passoword nữa thì tui đã disable nó đi rồi, cóp dán mất thời gian quá 🥲, bằng một dòng đơn giản như này thôi:
.httpBasic(hp -> hp.disable())
Giờ mình config chuẩn chỉnh lại coi:
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Apply CORS policy to all endpoints
.allowedOrigins("https://example.com", "https://viblo.asia") // Allowed origins
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // HTTP methods
.allowedHeaders("Authorization", "Content-Type") // Allowed headers
.allowCredentials(true); // Allow credentials (cookies, Authorization headers, etc.)
}
}
Anh em có thể thấy tui test GET
, OPTIONS
mọi thứ hoạt động quá đúng:
Tóm lại:
Đấy quá đơn giản để config cors ở mức global bằng WebMvcConfigurer
chỉ vài dòng đơn giản thôi, trong thực tế thì thường sẽ config như trên hơn là dùng trong controller, bạn chỉ cần cấu hình đúng một chỗ, khi nào có update gì chỉ cần sửa một chỗ thôi, quá dễ dàng.
3. Cấu hình trong Spring Security
Okay tiếp theo thì mình sẽ demo hướng dẫn anh em cấu hình cors trong Spring Security
nhé:
Anh em lưu lý import đúng package nhé, import nhầm thằng CorsConfigurationSource
thì nó sẽ lỗi nhé, đó là lý do tại sao tui đưa cả code import một nùi cho anh em luôn 😂
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.List;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("https://example.com", "https://viblo.asia")); // Allowed origins
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // HTTP methods
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); // Allowed headers
configuration.setAllowCredentials(true); // Allow credentials (cookies, Authorization headers, etc.)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // Apply this CORS policy to all endpoints
return source;
}
}
Rồi anh em chạy thử xem có chạy được không 😁😁
Note: What the heo, sao config đúng hết mà nó vẫn dính CORS nhỉ, như đã nói sơ qua trong phần đầu thì nếu anh em dùng
Spring Security
thì anh em phải bậthttp.cors()
thì thằng config trên nó mới ăn nhé anh em 🥹
Đơn giản như thế này thôi:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(withDefaults()) // Enable CORS
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // Allow access to everything else
)
.httpBasic(hp -> hp.disable()) // Enable HTTP Basic for authentication
.csrf(csrf -> csrf.disable()); // Disable CSRF for simplicity in the demo
return http.build();
}
}
Rồi perfect, hết lỗi hết bug.
Tóm lại và Cảm ơn
Xong bài viết đến đây là hết rồi, thì config Cors trong Spring Boot cũng không có gì quá khó khăn đúng không anh em, chỉ là khi nào nên dùng thằng nào thôi. Chủ yếu là tôi demo một số bug anh em có thể gặp khi cấu hình ở controller thôi, và từ đó thì anh em có các cách cấu hình khác.
Cuối cùng thì chào tạm biệt anh em, cảm ơn anh em đã đọc bài, nếu anh em cảm thấy bài viết mang lại giá trị cho anh em, thì anh em hãy upvote giúp mình để mình có thêm động lực viết thêm nhiều bài hơn nữa 🥹🥹🥹, Bên cạnh đó anh em gặp vấn đề gì hoặc thấy bug trong bài viết của mình xin hãy feedback giúp mình.
Thanks anh em ❤️
Sài Gòn, 15:06 22/12/2024
All rights reserved