Spring REST API & Data JPA
5/46
Bài 5 / 46~12 phútSpring MVC CoreLộ trình · chặng 15/21Miễn phí… lượt xem

Content Negotiation, API Versioning Và CORS Spring

Khám phá content negotiation qua produces/consumes, 3 chiến lược API versioning thực chiến và thiết lập CORS an toàn trong Spring Boot.

TL;DR: Spring MVC quản lý định dạng dữ liệu thông qua hai thuộc tính: produces (khớp với header Accept, trả về 406 nếu không tương thích) và consumes (khớp với header Content-Type, trả về 415 nếu không hỗ trợ). Khi xuất hiện nhiều handler cùng khớp một URL, hệ thống phân giải dựa trên thang độ đặc hiệu (specificity ranking). Để tránh làm gãy vỡ hệ thống của client khi nâng cấp dữ liệu, API versioning cung cấp 3 giải pháp gồm path, header và media-type, kết hợp cùng cơ chế CORS tập trung để bảo vệ tài nguyên web an toàn.

Nhớ lại — URL Routing Pipeline

Vì sao Content Negotiation giải quyết bài toán đa định dạng?

Trong kiến trúc RESTful chuẩn mực, URL đại diện cho tài nguyên độc lập (resource) chứ không đại diện cho hình thức biểu diễn (representation). Một endpoint duy nhất /api/orders/42 có thể phục vụ định dạng JSON cho ứng dụng web, XML cho hệ thống đối tác cũ và CSV cho công cụ báo cáo.

Content Negotiation cho phép client và server thương lượng định dạng thông qua các tiêu đề HTTP:

Java
// Same URL path handled by distinct representations
@GetMapping(value = "/api/orders/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public OrderDto getJson(@PathVariable Long id) {
    return orderService.findById(id);
}

@GetMapping(value = "/api/orders/{id}", produces = MediaType.APPLICATION_XML_VALUE)
public OrderDto getXml(@PathVariable Long id) {
    return orderService.findById(id);
}

Nếu client gửi request với Accept: application/json, Spring sẽ chọn phương thức getJson(). Nếu client yêu cầu Accept: text/csv mà không có handler nào khai báo hỗ trợ, Spring sẽ lập tức trả về mã lỗi 406 Not Acceptable.

Ngược lại, thuộc tính consumes quy định định dạng dữ liệu đầu vào:

Java
@PostMapping(value = "/api/upload", consumes = MediaType.APPLICATION_JSON_VALUE)
public void uploadJson(@RequestBody UploadRequest req) {
    storageService.storeJson(req);
}

Nếu client gửi dữ liệu thô với Content-Type: text/plain, Spring sẽ từ chối xử lý ngay lập tức với mã lỗi 415 Unsupported Media Type trước khi phân tích nội dung thân request.

Cây quyết định bốn tầng lọc cho một request POST: URL cùng HTTP method không khớp thì 404, Content-Type không khớp consumes thì 415 Unsupported Media Type, Accept không khớp produces thì 406 Not Acceptable, headers hoặc params không khớp thì cũng 404; qua hết bốn tầng mới sắp theo specificity rồi chạy handler

Thứ tự phân cấp độ đặc hiệu (specificity) diễn ra như thế nào?

Khi một đường dẫn URL có thể khớp với nhiều handler khác nhau, Spring không phụ thuộc vào thứ tự dòng mã được viết trong controller. Thay vào đó, framework phân định dựa trên chỉ số đặc hiệu (specificity ranking):

Request GET /api/orders/latest thả vào năm pattern xếp từ hẹp tới rộng: đoạn literal khớp và hẹp nhất nên thắng, biến có ràng buộc regex chỉ nhận chữ số nên không khớp, còn biến không ràng buộc, wildcard một đoạn và wildcard nhiều đoạn đều khớp nhưng rộng hơn

Thứ tự ưu tiên từ cụ thể nhất đến tổng quát nhất:

  1. Phân đoạn tĩnh (Literal segment): /orders/latest (Ưu tiên cao nhất).
  2. Biến đường dẫn có Regex: /orders/{id:\\d+}.
  3. Biến đường dẫn thông thường: /orders/{id}.
  4. Ký tự đại diện đơn (Single wildcard): /orders/*.
  5. Ký tự đại diện đa phân đoạn (Greedy wildcard): /orders/** (Ưu tiên thấp nhất).

Khi client gọi GET /api/orders/latest, handler có chuỗi tĩnh latest luôn được chọn trước, biến {id} sẽ không bị kích hoạt nhầm giá trị chuỗi.

3 chiến lược API Versioning thực chiến có ưu nhược điểm gì?

Khi ứng dụng cần nâng cấp cấu trúc dữ liệu trả về, việc thay đổi trực tiếp sẽ làm gãy vỡ hệ thống của các client đang vận hành. Có ba chiến lược phiên bản hóa chính:

  1. Phiên bản hóa theo đường dẫn URL (Path Versioning):
Java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 {
    @GetMapping("/{id}")
    public OrderDtoV1 findById(@PathVariable Long id) {
        return orderService.findByIdV1(id);
    }
}

Đây là mô hình phổ biến nhất (chiếm hơn 80% ứng dụng thực tế). Nó hiển thị phiên bản tường minh trên URL, dễ dàng kiểm thử bằng trình duyệt và tương thích tự nhiên với bộ nhớ đệm của mạng CDN.

  1. Phiên bản hóa qua Request Header:
Java
@GetMapping(value = "/api/orders/{id}", headers = "X-API-Version=2")
public OrderDtoV2 findByIdV2(@PathVariable Long id) {
    return orderService.findByIdV2(id);
}

Giữ cho đường dẫn URL ổn định không đổi. Thích hợp cho các hệ thống API nội bộ, nhưng đòi hỏi phải cấu hình tiêu đề Vary trên các tầng proxy trung gian.

  1. Phiên bản hóa theo Media Type (Content Negotiation Versioning):
Java
@GetMapping(value = "/api/orders/{id}", produces = "application/vnd.olhub.v2+json")
public OrderDtoV2 findByIdV2(@PathVariable Long id) {
    return orderService.findByIdV2(id);
}

Đây là phong cách thuần túy nhất theo chuẩn REST, nhưng gây phức tạp cho việc viết tài liệu kỹ thuật và xây dựng client SDK.

Khi lên kế hoạch dừng phục vụ phiên bản cũ, bạn nên bổ sung các tiêu đề thông báo lộ trình chuyển đổi:

Java
@GetMapping("/api/v1/orders/{id}")
public ResponseEntity<OrderDtoV1> getV1(@PathVariable Long id) {
    return ResponseEntity.ok()
        .header("Deprecation", "true")
        .header("Sunset", "2027-12-31")
        .body(orderService.findByIdV1(id));
}

Cơ chế bảo mật CORS và phương thức cấu hình chuẩn gồm những gì?

Cơ chế Same-Origin Policy (SOP) trên trình duyệt mặc định cấm mã JavaScript tải tài nguyên từ một nguồn gốc khác (khác giao thức, tên miền hoặc cổng kết nối). Cross-Origin Resource Sharing (CORS) là cơ chế để server cho phép các client được chỉ định truy cập tài nguyên.

Trong môi trường production, bạn nên tập trung cấu hình CORS tại một vị trí duy nhất thông qua WebMvcConfigurer:

Java
@Configuration
public class CorsGlobalConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://olhub.vn", "https://app.olhub.vn")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Authorization", "Content-Type", "X-Request-Id")
            .exposedHeaders("X-Total-Count")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

Cảnh báo bảo mật: Tuyệt đối không kết hợp allowedOrigins("*") cùng allowCredentials(true). Trình duyệt web coi đây là một lỗ hổng bảo mật nghiêm trọng và sẽ từ chối phản hồi ngay lập tức.

Chuỗi lọc điều kiện hoàn chỉnh của RequestMapping diễn ra ra sao?

Khi một request gửi tới DispatcherServlet, bộ xử lý kiểm tra theo chuỗi mắt xích nghiêm ngặt:

  1. Đối soát mẫu URL và phương thức HTTP (không khớp trả về 404 hoặc 405).
  2. Kiểm tra thuộc tính consumes đối soát với Content-Type (không khớp trả về 415).
  3. Kiểm tra thuộc tính produces đối soát với Accept (không khớp trả về 406).
  4. Kiểm tra điều kiện headers và params.
  5. Sắp xếp các ứng viên còn lại theo thang điểm đặc hiệu để chọn ra handler duy nhất.

Mọi kiểm tra đều diễn ra trước khi gọi phương thức controller, bảo vệ ứng dụng khỏi việc phân giải tham số không cần thiết khi client gửi sai định dạng.

Những cạm bẫy bảo mật và vận hành nào thường gặp nhất?

Dưới đây là các lưu ý thiết kế quan trọng:

  1. Quên khai báo consumes cho API nhận file hoặc JSON: Khiến request truyền sai định dạng vẫn đi sâu vào controller trước khi ném ra ngoại lệ đọc dữ liệu khó hiểu.
  2. Cấu hình CORS xung đột giữa class và global: Đặt @CrossOrigin trên controller sẽ ghi đè thiết lập tập trung, gây khó khăn cho việc kiểm soát chính sách bảo mật của hệ thống.
  3. Cắt giảm phiên bản API cũ không thông báo: Làm gãy vỡ ứng dụng của đối tác khi không gửi kèm tiêu đề Sunset trước ít nhất 3 đến 6 tháng.

Tóm tắt

  • Content Negotiation sử dụng produces và consumes để thương lượng định dạng đại diện cho cùng một tài nguyên.
  • Không khớp Content-Type trả về mã lỗi 415; không khớp Accept trả về mã lỗi 406.
  • Thứ tự so khớp URL luôn ưu tiên chuỗi tĩnh tường minh trước các biểu thức Regex hoặc Path Variable.
  • Path versioning là lựa chọn tối ưu cho hệ thống API công cộng nhờ tính trực quan và khả năng lưu cache CDN.
  • Quản lý chính sách CORS tập trung để ngăn ngừa rủi ro rò rỉ dữ liệu qua các kịch bản tấn công chéo nguồn gốc.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Một controller khai báo @GetMapping(value = '/orders/{id}', produces = 'application/json'). Nếu client gửi request với header Accept: text/xml, mã trạng thái HTTP nào sẽ được trả về?
  2. Q2
    Một endpoint upload nhận dữ liệu JSON được cấu hình consumes = 'application/json'. Nếu client gửi request có header Content-Type: text/plain, mã lỗi nào sẽ được phản hồi?
  3. Q3
    Tại sao chiến lược Path Versioning (/api/v1/orders) lại được sử dụng phổ biến nhất trong các hệ thống API công cộng thực tế?
  4. Q4
    Trong cấu hình CORS, vì sao việc kết hợp allowedOrigins('*') cùng allowCredentials(true) lại bị trình duyệt và Spring Boot ngăn cấm?
  5. Q5
    Tiêu đề HTTP nào theo chuẩn RFC 8594 được sử dụng để thông báo cho client về ngày giờ chính xác một phiên bản API sẽ chính thức ngừng hoạt động?

Bài tiếp theo: Tổng kết module 01

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

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 15/21 · REST API với Spring MVC — bài 5/21 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tổng Kết Module 01: Kiến Trúc Spring MVC Core