+1

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ắt credentials.
  • Để cấu hình chi tiết hơn, bạn cần sử dụng cấu hình global hoặc Spring 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ởi Spring 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ật authorized 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ật http.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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí