Spring REST API & Data JPA/OpenAPI nâng cao — security scheme, GroupedOpenApi, ẩn endpoint, production deploy, API-first
19/46
Bài 19 / 46~14 phútError, Validation & API DocsMiễn phí lượt xem

OpenAPI nâng cao — security scheme, GroupedOpenApi, ẩn endpoint, production deploy, API-first

Bài này đào sâu OpenAPI/springdoc nâng cao: khai báo security scheme JWT/OAuth2 trong spec để Swagger UI có nút Authorize, tách doc thành nhiều nhóm với GroupedOpenApi, ẩn endpoint nội bộ khỏi spec, chiến lược deploy production (tắt hay bảo vệ?), và phân tích trade-off API-first vs code-first cho team song song.

TL;DR: Bốn kỹ thuật nâng cao của springdoc: (1) @SecurityScheme khai báo JWT/OAuth2 trong spec — Swagger UI hiển thị nút "Authorize" để inject token vào mọi request "Try it out"; (2) GroupedOpenApi tách doc thành nhiều nhóm độc lập (public/admin/internal), mỗi nhóm có spec riêng tại /v3/api-docs/{group}; (3) @Hidden hoặc springdoc.paths-to-exclude ẩn endpoint nội bộ khỏi spec public; (4) production nên disable hoặc auth-protect Swagger UI — không để public vì lộ toàn bộ API surface. Cuối bài: so sánh API-first (design spec trước → backend + frontend implement song song) vs code-first (springdoc default) — biết chọn đúng theo quy mô team.

Bài trước (OpenAPI & springdoc) đã bóc cơ chế auto-generate spec và các annotation customize cơ bản (@Operation, @Schema, @ApiResponses). Bài này đi tiếp bốn bài toán thực tế mà mọi production app đều gặp: bảo mật doc, chia nhóm doc, ẩn endpoint nội bộ, và triết lý thiết kế API-first.

1. Security scheme — inject token vào Swagger UI

Vì sao cần khai báo security scheme trong spec?

Khi app có JWT authentication, developer cần test endpoint protected từ Swagger UI. Không có security scheme, UI chỉ gửi request bare — server từ chối 401. Developer phải mở Postman, copy-paste header thủ công — mất thời gian, dễ sai.

Security scheme là phần khai báo trong OpenAPI spec mô tả cơ chế xác thực mà API yêu cầu: loại scheme (HTTP Bearer, OAuth2, API Key...), vị trí token (header, cookie, query), và format (JWT, opaque). Khi spec có phần này, Swagger UI render nút "Authorize" — user paste token một lần, mọi "Try it out" sau đó tự inject header Authorization: Bearer <token>.

JWT Bearer scheme

// Khai bao scheme o cap @Configuration — springdoc doc vao components.securitySchemes
@Configuration
@SecurityScheme(
    name = "bearerAuth",                     // ten de ref trong @SecurityRequirement
    type = SecuritySchemeType.HTTP,
    scheme = "bearer",
    bearerFormat = "JWT",                    // chi doc, khong validate
    in = SecuritySchemeIn.HEADER,
    description = "JWT token tu /api/auth/login"
)
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info().title("TaskFlow API").version("v1.0"))
            // Ap dung global cho moi endpoint
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
    }
}

Khai báo trên thêm vào spec JSON tại components.securitySchemes:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - bearerAuth: []     # global requirement

Endpoint login không cần auth — override global bằng @SecurityRequirements rỗng:

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @SecurityRequirements   // empty — override global, khong can token
    @PostMapping("/login")
    public TokenResponse login(@Valid @RequestBody LoginRequest req) { ... }

    @SecurityRequirements   // tuong tu
    @PostMapping("/register")
    public UserDto register(@Valid @RequestBody RegisterRequest req) { ... }
}

// Protected controller — ke thua global hoac explicit
@SecurityRequirement(name = "bearerAuth")
@RestController
@RequestMapping("/api/projects")
public class ProjectController { ... }

Luồng sử dụng trong UI:

flowchart LR
  A["Mo /swagger-ui.html"] --> B["Click nut Authorize"]
  B --> C["Paste JWT token"]
  C --> D["Click Authorize trong modal"]
  D --> E["Try it out bat ky endpoint"]
  E --> F["UI tu inject<br/>Authorization: Bearer token"]
  F --> G["Server nhan request co token hop le"]

OAuth2 Authorization Code scheme

Khi app dùng OAuth2/OIDC (ví dụ tích hợp Google, Keycloak), khai báo scheme phức tạp hơn — spec mô tả đầy đủ các URL của auth server để UI thực hiện flow authorization code:

@SecurityScheme(
    name = "oauth2",
    type = SecuritySchemeType.OAUTH2,
    flows = @OAuthFlows(
        authorizationCode = @OAuthFlow(
            authorizationUrl = "https://auth.olhub.org/oauth2/authorize",
            tokenUrl = "https://auth.olhub.org/oauth2/token",
            scopes = {
                @OAuthScope(name = "read",  description = "Doc du lieu"),
                @OAuthScope(name = "write", description = "Tao va sua du lieu")
            }
        )
    )
)

UI redirect user sang auth server, nhận code callback, exchange lấy token — toàn bộ OAuth2 flow chạy trong trình duyệt qua Swagger UI.

Chọn scheme nào?

App internal dùng JWT Bearer: đơn giản, developer tự login lấy token rồi paste. App tích hợp identity provider bên ngoài (Google, Azure AD, Keycloak): dùng OAuth2 scheme để UI tự thực hiện redirect flow — không cần copy-paste token thủ công.

2. GroupedOpenApi — chia doc thành nhiều nhóm

Vì sao cần nhóm tách biệt?

App lớn có nhiều loại endpoint: public API cho khách hàng, admin API cho internal team, internal API cho service-to-service call. Nếu gộp hết vào một spec, developer public nhìn thấy toàn bộ admin endpoint — nguy hiểm về thông tin. Hơn nữa, spec khổng lồ (200+ endpoint) khó điều hướng.

GroupedOpenApi là bean springdoc tạo ra một spec độc lập cho một tập endpoint, filter theo path pattern hoặc package. Mỗi nhóm có URL spec riêng tại /v3/api-docs/{groupName}. Swagger UI hiển thị dropdown "Select Definition" — user chọn nhóm.

@Configuration
public class OpenApiGroupConfig {

    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
            .group("public")
            .displayName("Public API v1")
            .pathsToMatch("/api/v1/**")
            .build();
    }

    @Bean
    public GroupedOpenApi adminApi() {
        return GroupedOpenApi.builder()
            .group("admin")
            .displayName("Admin API (Internal)")
            .pathsToMatch("/api/admin/**")
            .addOpenApiCustomizer(openApi ->
                openApi.info(new Info()
                    .title("TaskFlow Admin API")
                    .version("v1")
                    .description("INTERNAL USE ONLY")))
            .build();
    }

    @Bean
    public GroupedOpenApi internalApi() {
        return GroupedOpenApi.builder()
            .group("internal")
            .displayName("Service-to-Service")
            .pathsToMatch("/internal/**")
            .build();
    }
}

Kết quả — ba spec endpoint độc lập:

flowchart TB
  subgraph CTRL["Controllers"]
    direction TB
    P1["/api/v1/** -- public"]
    P2["/api/admin/** -- admin"]
    P3["/internal/** -- service"]
  end
  subgraph GRP["GroupedOpenApi beans"]
    direction TB
    G1["group: public"]
    G2["group: admin"]
    G3["group: internal"]
  end
  subgraph SPEC["Spec URLs"]
    direction TB
    E1["GET /v3/api-docs/public"]
    E2["GET /v3/api-docs/admin"]
    E3["GET /v3/api-docs/internal"]
  end
  P1 --> G1 --> E1
  P2 --> G2 --> E2
  P3 --> G3 --> E3

Swagger UI có dropdown "Select Definition" top-right — chọn nhóm nào, spec tương ứng nhóm đó được load. Mỗi nhóm có thể có security scheme, server, info title riêng qua addOpenApiCustomizer.

Bảo vệ selectively theo group — production best practice:

// Chỉ public spec không cần auth; admin + internal yêu cầu ROLE_ADMIN
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/v3/api-docs/public").permitAll()
        .requestMatchers("/v3/api-docs/admin", "/v3/api-docs/internal")
            .hasRole("ADMIN")
        .anyRequest().authenticated()
    );
    return http.build();
}

3. Ẩn endpoint khỏi spec — @Hidden và config exclude

Không phải mọi endpoint đều cần xuất hiện trong doc. Actuator endpoints, debug endpoints, migration scripts endpoint — tất cả nên ẩn khỏi spec trước khi deploy.

@Hidden — ẩn chọn lọc

// An toan bo mot endpoint
@Hidden
@GetMapping("/internal/health-check-verbose")
public Map<String, Object> verboseHealth() { ... }

// An toan bo mot controller
@Hidden
@RestController
@RequestMapping("/debug")
public class DebugController {
    // Khong co endpoint nao trong class nay xuat hien trong spec
}

@Hidden từ io.swagger.v3.oas.annotations — springdoc skip endpoint này khi build spec, dù controller vẫn hoạt động bình thường ở runtime.

Config exclude — ẩn theo pattern

Khi cần ẩn nhiều endpoint theo pattern, dùng config thay vì annotate từng method:

springdoc:
  paths-to-exclude: /internal/**, /debug/**, /actuator/**
  packages-to-exclude: com.olhub.internal, com.olhub.debug
  show-actuator: false   # Boot actuator endpoints luon an
Pitfall — @Hidden che giấu khỏi doc, không phải khỏi truy cập

@Hidden chỉ loại endpoint ra khỏi OpenAPI spec — endpoint vẫn hoạt động và ai biết URL đều gọi được. Để bảo vệ thật sự, cần Spring Security URL rule hoặc method security. Xem bài Spring Security khi học module security.

4. Production deploy — tắt hay bảo vệ Swagger UI?

Vì sao đây là quyết định bảo mật, không phải preference?

Swagger UI trên production public có nghĩa: bất kỳ ai trên internet cũng thấy toàn bộ API surface của hệ thống — mọi endpoint, parameter, schema, constraint validation. Đây là thông tin attacker dùng để:

  • Enumerate attack surface: biết chính xác endpoint nào tồn tại, method nào hợp lệ, format payload ra sao.
  • Leak validation rule: spec expose constraint (minLength=8, pattern=...) giúp attacker design payload bypass hoặc brute-force có hướng.
  • Try-it-out as probe tool: "Try it out" gửi request thật đến production server — attacker dùng UI như công cụ fuzzing miễn phí.

Ba chiến lược, mức bảo vệ tăng dần:

Option 1 — Disable hoàn toàn trong production:

# application-prod.yml
springdoc:
  api-docs:
    enabled: false
  swagger-ui:
    enabled: false

Doc public được generate từ snapshot staging, host trên CDN riêng (Redocly, Stoplight Elements, GitHub Pages). App server không expose spec endpoint nào.

Option 2 — Auth-protect (recommended khi cần internal access):

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers(
            "/v3/api-docs/**",
            "/swagger-ui/**",
            "/swagger-ui.html")
        .hasRole("DEVELOPER")        // chỉ internal team
        .anyRequest().authenticated()
    );
    return http.build();
}

Internal developer login với account DEVELOPER role mới xem được doc. Không public.

Option 3 — Path obscurity (yếu nhất, không recommend):

springdoc:
  swagger-ui:
    path: /internal-docs-k9x2q   # URL kho doan
  api-docs:
    path: /internal-spec-k9x2q

Security through obscurity — không phải defense thật sự. Attacker scan phát hiện được.

Khuyến nghị cho TaskFlow capstone:

EnvironmentChiến lược
Local devEnabled, không auth
StagingEnabled, auth-protect (DEVELOPER role)
ProductionDisabled. CI/CD snapshot spec từ staging → host CDN

Snapshot spec trong CI/CD

# .github/workflows/deploy.yml (đoạn them vao sau build)
- name: Snapshot OpenAPI spec
  run: |
    # Chay app staging, lay spec
    curl -f http://staging.olhub.org/v3/api-docs > docs/api/openapi-v${{ env.VERSION }}.json
    git add docs/api/openapi-v${{ env.VERSION }}.json

Track diff giữa version bằng openapi-diff — phát hiện breaking change (xoá endpoint, đổi field bắt buộc).

5. API-first vs code-first — trade-off theo quy mô team

Hai workflow thiết kế API

Đây không phải lựa chọn đúng-sai — là lựa chọn phù hợp ngữ cảnh.

Code-first (springdoc default): developer code @RestController trước, springdoc auto-generate spec từ annotation. Spec là output của quá trình implement.

API-first: team design OpenAPI spec (openapi.yaml) trước — đây là contract. Tool sinh interface Java từ spec. Backend implement interface. Frontend dev song song với mock server đọc spec.

Vì sao API-first ra đời — bài toán parallel team

Code-first có vấn đề căn bản: frontend phải đợi backend code xong mới có spec để implement. Với team 2-3 người, đây chấp nhận được. Với team 10+ người gồm frontend và backend riêng, mất 1-2 tuần đợi backend scaffold là lãng phí.

API-first giải quyết bằng cách đảo thứ tự: spec ra đời trước, cả hai team dùng spec làm nguồn sự thật. Frontend dev với mock server (Prism chạy spec như real server), backend implement interface do generator sinh ra từ spec. Hai team song song từ ngày 1.

flowchart TB
  subgraph CF["Code-first (springdoc)"]
    direction LR
    CF1["Backend code controller"] --> CF2["springdoc gen spec"] --> CF3["Frontend nhan spec"]
    CF3 --> CF4["Frontend implement"]
    style CF3 fill:#fef3c7,stroke:#f59e0b
  end
  subgraph AF["API-first"]
    direction LR
    AF1["Design openapi.yaml"] --> AF2["Generator sinh interface"]
    AF1 --> AF3["Prism mock server"]
    AF2 --> AF4["Backend implement"]
    AF3 --> AF5["Frontend dev voi mock"]
    AF4 -.->|"tich hop"| AF5
  end

So sánh trade-off

Tiêu chíCode-firstAPI-first
Khi có specSau khi code xongTrước khi code
Frontend đợi backendKhông — mock server
Contract driftKhông — code là source of truthCó — spec và code có thể lệch nếu không enforce
Setup1 dependencyopenapi-generator plugin + spec.yaml
Chất lượng specAuto, có thể không idiomaticViết tay — có thể rất tốt hoặc rất xấu
Refactor codeSpec tự updatePhải update spec → regenerate → fix backend
Lint specKhó (spec là output)Dễ — Spectral lint openapi.yaml CI
Phù hợpTeam 1-5 người, iteration nhanhTeam 5+ người, API public/external, contract critical

API-first với openapi-generator-maven-plugin

<!-- pom.xml -->
<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.10.0</version>
    <executions>
        <execution>
            <goals><goal>generate</goal></goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <apiPackage>com.olhub.generated.api</apiPackage>
                <modelPackage>com.olhub.generated.model</modelPackage>
                <configOptions>
                    <interfaceOnly>true</interfaceOnly>      <!-- chi sinh interface, khong impl -->
                    <useSpringBoot3>true</useSpringBoot3>
                    <useJakartaEe>true</useJakartaEe>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

Generator sinh interface ProjectsApi từ spec. Controller implement interface — nếu spec thay đổi, compile fail ngay:

// Generated tu spec — KHONG edit file nay
public interface ProjectsApi {
    ResponseEntity<ProjectDto> getProject(@PathVariable Long id);
    ResponseEntity<ProjectDto> createProject(@Valid @RequestBody ProjectRequest req);
    ResponseEntity<Void> deleteProject(@PathVariable Long id);
}

// Controller implement interface — code do team viet
@RestController
public class ProjectController implements ProjectsApi {

    @Override
    public ResponseEntity<ProjectDto> getProject(Long id) {
        return ResponseEntity.ok(projectService.findById(id));
    }

    // ... implement cac method khac
}
Khoá này dùng code-first

TaskFlow capstone (bài tiếp theo) dùng code-first — đơn giản, phù hợp học một mình. API-first sẽ xuất hiện ở course Spring Reactive & Microservices khi hai service cần contract chặt chẽ và team chia ra. Hiểu trade-off ngay từ đây để nhận ra lúc nào nên chuyển.

Pitfall thực tế

Nhầm 1 — @Hidden là security control:

// SAI: tuong rang @Hidden bao ve endpoint
@Hidden
@GetMapping("/admin/delete-all")
public void deleteAll() { ... }

@Hidden chỉ ẩn khỏi doc. Endpoint vẫn hoạt động — ai đoán được URL vẫn gọi được. Bảo vệ thật sự cần @PreAuthorize hoặc Spring Security URL rule.

✅ Dùng @Hidden để giữ doc gọn, dùng security annotation/config để bảo vệ thật sự.

Nhầm 2 — GroupedOpenApi không filter, spec vẫn expose hết:

// SAI: group khong co pathsToMatch -> include tat ca endpoint
@Bean
public GroupedOpenApi publicApi() {
    return GroupedOpenApi.builder()
        .group("public")
        .build();  // thieu pathsToMatch
}

✅ Luôn khai báo pathsToMatch hoặc packagesToScan rõ ràng cho mỗi group.

Nhầm 3 — Để security scheme global nhưng quên exempt public endpoint:

Khi dùng .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) global, endpoint login cũng yêu cầu token trong spec — Swagger UI lock nút "Try it out" cho /auth/login. Phải thêm @SecurityRequirements rỗng cho endpoint không cần auth.

Liên hệ các bài khác

  • OpenAPI & springdoc: bài trước — cơ chế auto-generate spec, annotation @Operation/@Schema/@ApiResponses. Bài này là layer nâng cao lên trên nền đó.
  • Mini-challenge TaskFlow v1: capstone module — áp dụng toàn bộ kiến thức module 03 bao gồm security scheme JWT và config production. Thực hành ngay sau bài này.
  • Exception handling và validation (các bài trước trong module 03): spec tự document ProblemDetail response từ @RestControllerAdvice — thấy rõ liên hệ khi setup @ApiResponses cho error codes.

Tóm tắt

  • Security scheme (@SecurityScheme + @SecurityRequirement) khai báo JWT/OAuth2 trong spec — Swagger UI hiển thị nút "Authorize", inject token tự động vào mọi "Try it out". Endpoint public override bằng @SecurityRequirements rỗng.
  • GroupedOpenApi tách spec thành nhiều nhóm (public/admin/internal) — mỗi nhóm có URL spec riêng /v3/api-docs/{group}, có thể auth-protect chọn lọc.
  • @Hidden ẩn endpoint khỏi spec (không phải khỏi truy cập). Config springdoc.paths-to-exclude ẩn theo pattern. @Hidden không phải security control.
  • Production: disable Swagger UI hoặc auth-protect. Public doc host trên CDN từ snapshot staging. Không expose spec endpoint public trên app server production.
  • Code-first (springdoc): spec là output của code — phù hợp team nhỏ, iteration nhanh. API-first (openapi-generator): spec là contract thiết kế trước — backend + frontend implement song song, phù hợp team lớn, API public.

Tự kiểm tra

Tự kiểm tra
Q1
Developer paste JWT vào Swagger UI và click "Authorize". Cơ chế nào trong spec khiến UI biết cần hiển thị ô nhập token, và token đó được gắn vào request ra sao?

Khi spec có phần components.securitySchemes với type http, scheme bearer, Swagger UI đọc phần này lúc load và render nút "Authorize" (biểu tượng ổ khóa). Phần security ở root spec (hoặc per-endpoint) nói UI biết endpoint nào yêu cầu scheme đó.

Khi user paste token và click "Authorize", UI lưu token vào session storage của trình duyệt. Mỗi khi user click "Try it out" rồi "Execute", UI tự thêm header Authorization: Bearer eyJhbGc... vào HTTP request gửi đi — không khác gì Postman set header thủ công, chỉ là UI tự làm.

Về phía Spring: annotation @SecurityScheme trên @Configuration class khiến springdoc thêm entry vào components.securitySchemes khi build spec. addSecurityItem(new SecurityRequirement().addList("bearerAuth")) trong bean OpenAPI thêm phần security: [{bearerAuth: []}] global. Endpoint dùng @SecurityRequirements rỗng sẽ override global — không có security requirement trong spec cho endpoint đó.

Q2
Tại sao @Hidden không đủ để bảo vệ endpoint admin? Cần thêm gì để bảo vệ thật sự?

@Hidden chỉ ra lệnh cho springdoc bỏ endpoint đó ra khỏi OpenAPI spec khi build. Endpoint vẫn được đăng ký vào DispatcherServlet và hoạt động bình thường ở runtime — ai biết URL đều gọi được bằng curl hoặc Postman.

Về cơ chế: springdoc là thư viện tạo doc, không phải security filter. Nó chạy lúc startup để scan annotation và build spec object. Spring Security hoạt động hoàn toàn tách biệt ở tầng filter chain — springdoc không có quyền block hay cho phép request.

Bảo vệ thật sự cần một trong hai: (1) Spring Security URL rule — requestMatchers("/admin/**").hasRole("ADMIN") trong SecurityFilterChain; hoặc (2) method security — @PreAuthorize("hasRole('ADMIN')") trên method controller. Cả hai chặn ở runtime, không phải ở tầng doc. Pattern đúng: dùng @Hidden để giữ doc gọn, dùng security annotation để bảo vệ thật.

Q3
App có 150 endpoint: 80 public, 50 admin, 20 internal service-to-service. Giải thích cách setup GroupedOpenApi và chiến lược bảo vệ spec endpoint trong production.

Tạo ba bean GroupedOpenApi trong một @Configuration class, mỗi bean filter theo path pattern:

  • public: pathsToMatch("/api/v1/**") — 80 endpoint public.
  • admin: pathsToMatch("/api/admin/**") — 50 endpoint admin.
  • internal: pathsToMatch("/internal/**") — 20 endpoint service-to-service.

Mỗi group có spec URL riêng: /v3/api-docs/public, /v3/api-docs/admin, /v3/api-docs/internal.

Chiến lược bảo vệ production: trong SecurityFilterChain, /v3/api-docs/public có thể permitAll() (nếu cần external developer đọc), còn /v3/api-docs/admin/v3/api-docs/internal yêu cầu hasRole("ADMIN"). Swagger UI endpoints (/swagger-ui/**) cũng auth-protect hoặc disable hoàn toàn. Public API doc tốt nhất host trên CDN từ snapshot staging — tách hoàn toàn khỏi app server production.

Q4
Team gồm 3 backend và 3 frontend developer cần deliver TaskFlow v2 trong 2 tuần. Backend ước tính 1 tuần để scaffold API. Code-first hay API-first phù hợp hơn? Vì sao?

API-first phù hợp hơn cho scenario này vì vấn đề then chốt là thời gian chờ đợi (blocking time).

Với code-first: tuần 1 backend code controller, tuần 2 frontend mới có spec để implement. Thực tế frontend bị block 1 tuần, chỉ còn 1 tuần implement — rất căng, dễ miss deadline.

Với API-first: ngày 1-2, tech lead (hoặc cả team) design openapi.yaml — định nghĩa endpoint, schema, status code. Ngày 3: backend chạy openapi-generator sinh interface, bắt đầu implement; frontend chạy Prism mock server (prism mock openapi.yaml --port 4010), bắt đầu implement UI gọi mock. Hai team song song từ ngày 3, không block nhau.

Overhead của API-first là thiết kế spec kỹ ở đầu sprint — nhưng 1-2 ngày design là xứng đáng so với 1 tuần frontend ngồi chờ. Trade-off chính: nếu spec thay đổi giữa sprint, cả 2 team phải review và align lại — cần discipline hơn. Với team 3+3 trong 2 tuần, API-first thắng rõ.

Q5
Production app của bạn vừa bị pentest phát hiện lỗ hổng: Swagger UI public accessible tại /swagger-ui.html, expose toàn bộ 120 endpoint. Liệt kê 3 rủi ro cụ thể và 2 cách khắc phục có thể apply ngay.

3 rủi ro cụ thể:

  1. Enumeration attack surface: attacker thấy đầy đủ 120 endpoint — biết chính xác URL, method, parameter. Thay vì phải guess, attacker có roadmap hoàn chỉnh để probe từng endpoint với payload có chủ đích.
  2. Validation rule leak: @Schema và validation annotation expose constraint vào spec — ví dụ password minLength: 8, pattern: "^[A-Z].*". Attacker dùng thông tin này để design payload bypass hoặc brute-force có hướng hơn.
  3. "Try it out" như công cụ fuzzing: Swagger UI gửi request thật đến production server. Attacker dùng UI thử payload trực tiếp — DDoS-light, probe error response, test rate limit — mà không cần viết code.

2 cách khắc phục ngay (theo thứ tự ưu tiên):

  1. Disable hoàn toàn production (nhanh nhất, 2 dòng config):
    # application-prod.yml
    springdoc:
    api-docs:
      enabled: false
    swagger-ui:
      enabled: false
    Restart app. Endpoint /swagger-ui.html/v3/api-docs trả 404. Zero risk.
  2. Auth-protect nếu cần internal access: thêm requestMatchers("/v3/api-docs/**", "/swagger-ui/**").hasRole("DEVELOPER") vào SecurityFilterChain. Internal team đăng nhập với tài khoản DEVELOPER role mới xem được. Attacker không có credential không access được.

Sau khắc phục: public API doc (nếu cần) host trên CDN riêng từ snapshot spec staging — tách hoàn toàn khỏi app server production, không có "Try it out" vào production.

Bài tiếp theo: Mini-challenge — TaskFlow REST API v1

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

Đặt 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