Nhảy đến nội dung chính

3.Cơ chế xác thực (JWT/Bearer)

3.1. Mục tiêuTiêu chương

Chương

Chương này quy định chuẩn codingxác thực và nguyên tắc thiết kế bảo mật nhằm:

  • Đảm bảo codecơ chế xác thực thống nhất giữa các moduleintegration flow các đơn vị phát triển

  • Giúp codeartifact dễ đọc, dễ hiểu và dễ bảo trì

  • Giảm lỗi phát sinh do cáchxử đặt tên hoặc codingtoken không đồngđúng nhất

    chuẩn
  • Tăng hiệu quả review và kiểm soát chất lượng code

    artifact
  • Đảm bảo codetoàn tuânbộ thủluồng nguyêntừ Client thiết kếAPIM SOLID

  • MI
  • ĐảmBackend bảođều kiến trúcxác backend Spring Bootthực nhất quán

  • toàn
  • Tích hệhợp thống

    đúng với WSO2 Identity Server (IS) làm trung tâm cấp và xác thực token

3.2. Khái niệmNiệm / phạmPhạm viVi ápÁp dụng

Dụng

Quy định này áp dụng cho:

  • Toàn bộ sourceartifact codexử của hệhoặc thống

    chuyển tiếp request qua xác thực
  • Core Team và Partner Team

  • Tất cả các moduleAPI, Sequence, Inbound Endpoint trong modules/artifacts/**

  • Cấu

    Cáchình thành phần trong common/**APIMconfig/**IS do Core Team quản lý

  • Tất cả service, controller, repository, DTO, entity


Kiến Trúc Xác Thực Tổng Thể

image.png

Ba Tầng Xác Thực

TầngThành phầnVai trò xác thực
Cấp tokenWSO2 Identity Server (IS)OAuth2 Authorization Server, cấp JWT/Access Token
Validate tokenWSO2 API Manager (APIM)Xác thực token tại Gateway trước khi route vào MI
Forward tokenWSO2 Micro Integrator (MI)Set Authorization: Bearer header khi gọi backend

3.3. Quy địnhĐịnh chính

Chính

3.3.1. Quy tắcTắc đặtĐặt tên

Tên Artifact

Các thànhartifact phầnliên quan xác thực phải đặt tên theo quy ước thống nhất:

Thành phần Quy tắc đặt tên Ví dụ
ControllerREST API <Name>ControllerApi.xml UserController

KafkaProducerApi.xml

PlanningDirectApi.xml
ServiceInbound Sequence<flow>-inboundSequence.xml

Load_balance_example-inboundSequence.xml
Error Sequence<flow>-inboundErrorSequence.xml

Load_balance_example-inboundErrorSequence.xml
Inbound Endpoint<flow>.xml

Load_balance_example.xml
Connection Entry <Name>ServiceConnection.xml UserService

KafkaConnection.xml
RepositoryBackend Endpoint <Name>RepositoryEndpoint.xml UserRepository
Entity<Name>EntityUserEntity
DTO Request<Name>CreateRequest, <Name>UpdateRequestUserCreateRequest
DTO Response<Name>ResponseUserResponse
Mapper<Name>MapperUserMapper
Enum<Name>EnumUserStatusEnumPlanningBackendEndpoint.xml

3.3.2. Quy tắcTắc chungĐặt Java

Property Trong Sequence

Dùng camelCasetên cho:property nhất quán khi xử lý token/header:

    <!-- 
  • Đúng:

    Biến

    Tên
  • property
  • Method

Dùng PascalCase cho:

  • Class

  • Interface

  • Enum

Ví dụ:


private String fullName; //ràng, đúng privatechuẩn String--> FullName;<property name="PAYLOAD" expression="json-eval($)" scope="default"/> <property name="HTTP_STATUS" expression="$axis2:HTTP_SC" scope="default"/> sai<property publicname="ERROR_CODE" classexpression="get-property('ERROR_CODE')" UserServicescope="default"/> {<property }name="ERROR_MESSAGE" expression="get-property('ERROR_MESSAGE')" scope="default"/> <!-- Sai: Tên không mô tả ý nghĩa --> <property name="x" expression="json-eval($)" scope="default"/> đúng<property publicname="code" classexpression="$axis2:HTTP_SC" userService { } scope="default"// sai>

3.3.3. Quy địnhĐịnh vềVề logging

Logging Xác Thực

Không được dùng:bỏ qua bước log khi xử lý request:

<!-- 
Sai:
Không

log
trạng
thái
System.out.println(--> <call> <endpoint> <address uri="Error");http://192.168.0.133:8080/api/v1/plannings"/> </endpoint> </call>

Phải dùnglog loggingđầy chuẩn:đủ các bước:

<!-- 
Đúng:
Log

đầy
đủ
theo
privatechuẩn staticdự finalán Logger--> <log =level="custom"> LoggerFactory.getLogger(UserService.class);<property log.info(name="UserAPI_NAME" created"value="PlanningDirectApi"/> <property name="STATUS" value="Nhận request, gọi thẳng Backend..."/> <property name="PAYLOAD" expression="get-property('PAYLOAD');"/> log.error("Create user failed", ex);</log>

 


3.3.4. ValidationQuy dữĐịnh liệuVề đầuToken vào

/ Header Xác Thực

TấtToken cảdùng dữđể liệugọi đầu vàobackend phải được validateset bằngtừ annotation.local-entries hoặc config.properties — không hardcode trong Sequence:

Annotation

<!-- chuẩn:

SAI:
    Hardcode
  • token

    @NotNull

    trong
  • Sequence
  • -->

    @NotBlank

    <header
  • name="Authorization"
  • scope="transport"

    @Size

    value="Bearer
  • @Email

  • @Pattern

Ví dụ:


public class UserCreateRequest { @NotBlank @Size(max = 100) private String username; @NotBlank private String password; }eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9..."/>
<!-- 
ĐÚNG:

Lấy token từ property/config --> <property name="BACKEND_TOKEN" expression="get-property('conf:backend.auth.token')" scope="default"/> <header name="Authorization" scope="transport" expression="fn:concat('Bearer ', get-property('BACKEND_TOKEN'))"/>

3.4. Nguyên Lý Tách Biệt Trách Nhiệm (Separation of Concerns)

Tương đương nguyên lý thiết kế SOLID áp dụng trong dựhệ ánthống

WSO2:

3.4.1. S — SingleMỗi Responsibility

Artifact Một Trách Nhiệm

MộtMỗi classartifact chỉ được thực hiện một tráchnhiệm nhiệm.vụ:

ArtifactChỉ được làm
apis/Nhận HTTP request, set header auth, gọi backend/Kafka, trả response
sequences/*-inboundSequence.xmlĐiều phối luồng message từ Kafka đến backend
sequences/*-inboundErrorSequence.xmlGhi nhận lỗi và publish vào error_topic
inbound-endpoints/Kết nối Kafka, subscribe topic
local-entries/Lưu thông tin kết nối và cấu hình dùng chung

QuySai định dựError án:logic nằm trong inbound sequence:

    <!-- 
  • SAI:

    ControllerInbound sequence chỉtự xử lý HTTPlỗi kỹ thuật --> <sequence name="myFlow-inboundSequence"> <call>...</call> <property name="ERROR_CODE" expression="get-property('ERROR_CODE')"/> <kafkaTransport.publishMessages configKey="KafkaConnection"> <topic>error_topic</topic> </kafkaTransport.publishMessages> <!-- Đây là nhiệm vụ của inboundErrorSequence --> </sequence>

  • Đúng — Error sequence riêng biệt:

    <inboundEndpoint
        
  • sequence="myFlow-inboundSequence"

    Service → xử lý nghiệp vụ

  • Repository → truy cập DB

  • Mapper → convert DTO ↔ Entity

Sai:


public class UserController { @Autowired UserRepository repo; // sai tầng }onError="myFlow-inboundErrorSequence">

Đúng:


public class UserController { @Autowired UserService userService; }

3.4.2. O — Open/Closed

Mở Rộng Không Sửa Artifact Cũ

CodeKhi phảithêm mởtopic/luồng rộngmới, đượctạo nhưngartifact mới — không sửa codesequence cũ.của luồng khác:

Áp

<!-- dụng:

Thêm
    luồng
  • mới:

    Dùngtạo interfacefile Service

    riêng
  • -->
  • <!--

    Khôngplanning_topic-inboundSequence.xml sửa--> class<!-- gốcplanning_topic-inboundErrorSequence.xml khi--> thêm<!-- logic

    planning_topic.xml
  • (inbound
endpoint)

Ví dụ:


public interface NotificationService { void send(Message msg); }-->

Triển khai:


public class EmailNotificationService implements NotificationService { } public class SmsNotificationService implements NotificationService { }

3.4.3. L — LiskovConsumer Substitution

Tuân Thủ Contract Sequence

ClassInbound con có thể thay thế class cha.

Quy định:

  • ServiceImplEndpoint phải tuântrỏ interfaceđúng sequence và error sequence đã khai báo:

  • <!-- 
  • Sai:

    KhôngonError throwtrỏ exceptionsai mớisequence trái--> contract

    <inboundEndpoint
  • name="Load_balance_example"
sequence="Load_balance_example-inboundSequence"

Sai:

onError="someOtherSequence"/>
<!--
không
tồn

tại
public class UserServiceImpl implements UserService { public User get(String id) { throw new UnsupportedOperationException(); } }-->
<!-- 
Đúng

--> <inboundEndpoint name="Load_balance_example" sequence="Load_balance_example-inboundSequence" onError="Load_balance_example-inboundErrorSequence"/>

3.4.4. I — InterfaceTách Segregation

Riêng Local Entry Theo Chức Năng

Không tạonhét interfacetất quácả lớn.cấu hình vào một local entry:

<!-- Sai: Một local entry chứa tất cả -->
<localEntry key="AllConfig">
    <!-- Kafka config, backend URL, token, ... -->
</localEntry>

<!-- Đúng: Tách riêng -->
<localEntry key="KafkaConnection">...</localEntry>
<localEntry key="BackendConfig">...</localEntry>

Sai: 


public interface UserService { create(); update(); delete(); exportExcel(); sendEmail(); }

Đúng:


public interface UserService { } public interface UserExportService { } public interface UserNotificationService { }

3.4.5. D — DependencyPhụ Inversion

Thuộc Config, Không Hardcode

Phụ thuộc abstraction, khôngSequence phụ thuộc implementation.vào local-entries và config.properties — không hardcode URL hay token:

Quy

<!-- định:

Sai
    -->
  • <address

    Injecturi="http://192.168.0.133:8080/api/v1/plannings"/> interface

    <!--
  • Đúng
  • -->

    Không<address new service

Sai:


UserService service = new UserServiceImpl();uri="{$ctx:backend.planning.url}"/>

Đúng:


@Autowired UserService userService;

3.5. QuyLuồng tắcXác kiếnThực trúcChuẩn SpringTrong BootHệ trongThống

dự án

3.5.1. Luồng chuẩnChuẩn tầng

Từ
Client
Đến
Backend

ControllerClientService[1] Lấy token từ WSO2 IS (/oauth2/token)Repository[2] Gọi API qua WSO2 APIM với Bearer tokenDatabase[3] 
APIM
validate

Khôngtoken được:

với
WSO2
IS
(introspect

/
JWT
Controllerverify)Repository[4] ControllerAPIM route request vào WSO2 MIEntityManager[5] MI set header Authorization khi gọi backend → [6] Backend (Spring Boot) validate JWT

3.5.2. ControllerLuồng Kafka Consumer Không Đi Qua APIM

Kafka Topic (test_topic_01)
  → [1] MI Inbound Endpoint consume message
  → [2] MI Inbound Sequence xử lý
  → [3] MI set header Authorization: Bearer <token>
  → [4] MI gọi Backend API
  → [5] Backend validate JWT
  → [6] Kết quả: published_topic hoặc error_topic

Luồng Kafka consumer bypass APIM Gateway — token phải được quản lý thủ công trong MI (lấy từ IS hoặc dùng service account token).

3.5.3. Không Được Phép

<!-- Sai: Gọi backend không chứa nghiệpAuthorization vụheader -->
<call>
    <endpoint>
        <address uri="http://192.168.0.133:8080/api/v1/plannings"/>
    </endpoint>
</call>

<!-- Sai: API tại APIM không bật Security Scheme -->
<!-- Mọi API publish lên APIM đều phải bật OAuth2 hoặc API Key -->

3.5.4. Luồng Token Trong Kafka Consumer (Chuẩn)

<!-- Bước 1: Lấy token từ config -->
<property name="BACKEND_TOKEN"
    expression="get-property('conf:backend.auth.token')"
    scope="default"/>

<!-- Bước 2: Set Authorization header -->
<header name="Authorization" scope="transport"
    expression="fn:concat('Bearer ', get-property('BACKEND_TOKEN'))"/>
<property name="Content-Type" value="application/json" scope="transport"/>

<!-- Bước 3: Gọi backend -->
<call>
    <endpoint>
        <address uri="{$ctx:backend.planning.url}">
            <suspendOnFailure>
                <initialDuration>1000</initialDuration>
                <progressionFactor>2.0</progressionFactor>
                <maximumDuration>60000</maximumDuration>
            </suspendOnFailure>
        </address>
    </endpoint>
</call>

3.6. Cấu Hình Xác Thực WSO2 APIM & IS

3.6.1. WSO2 IS — Key Manager

Sai:WSO2 IS đóng vai trò Key Manager cho APIM:


Usercreate
Chức năng Endpoint
WSO2 IS
Lấy Access Token@PostMappingPOST public/oauth2/token
Introspect TokenPOST /oauth2/introspect
Lấy Public Key (JWKS)GET /oauth2/jwks
Revoke TokenPOST /oauth2/revoke

Lấy token từ IS (client_credentials):

POST https://<IS_HOST>:9443/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(clientId:clientSecret)>

grant_type=client_credentials&scope=<scope>

 

3.6.2. WSO2 APIM — API Gateway Security

Mọi API publish lên APIM phải cấu hình:

Thuộc tínhGiá trị chuẩn
Security TypeOAuth2 hoặc API Key
Token ValidationIS Introspection / JWT verify
ThrottlingTheo chính sách dự án
CORSCấu hình theo môi trường

3.6.3. JWT Claims Chuẩn Của Hệ Thống

Token JWT cấp bởi WSO2 IS phải chứa:

{
  "sub": "admin",
  "role": "ADMIN",
  "iat": 1771917707,
  "exp": 1772004107,
  "iss": "https://<IS_HOST>:9443/oauth2/token",
  "aud": "...)client_id...",
  {"scope": user.setCreatedDate(LocalDateTime.now()); // sai"..."
}

3.7. Cách Thực Hiện / Quy Trình

Quy Trình Thêm Xác Thực Cho Luồng Mới

Đúng:

Bước
1: 
Đăng

OAuth2
Application
trong
@PostMappingWSO2 public UserResponse create(...) { return userService.create(request); }

3.5.3. Service không chứa logic HTTP

Sai:


throw new ResponseStatusException(HttpStatus.BAD_REQUEST);

Đúng:


throw new BusinessException("USER_EXISTS");

3.5.4. Repository chỉ truy vấn dữ liệu

Không chứa:IS

  • Tạo

    validation

    Service Provider
  • Lấy 

    businessclientId logic

     
  • mapping

    clientSecret

Bước

3.6.2: DesignCấu patternhình sử dụngtoken trong hệ

thống

backend.auth.token=<access_token_từ_IS>

3.6.1.backend.planning.url=http://192.168.0.133:8080/api/v1/plannings Service pattern

ServiceBước 3: tầngTrong nghiệpSequence, vụđọc trungtoken tâm.từ config

<property 
name="BACKEND_TOKEN"
expression="get-property('conf:backend.auth.token')"/>

<header
name="Authorization"
scope="transport"
Controllerexpression="fn:concat('Bearer ', Service → Repositoryget-property('BACKEND_TOKEN'))"/>

3.6.2. DTO pattern

Tách DTO khỏi Entity.

Không trả Entity ra API.

Sai:


public UserEntity create(...) { }

Đúng:


public UserResponse create(...) { }

3.6.3. Mapper pattern

Convert DTO ↔ Entity.


UserEntity entity = mapper.toEntity(request);

3.6.4. Exception pattern

Dùng BusinessException thống nhất.


throw new BusinessException(ErrorCode.USER_NOT_FOUND);

3.7. Cách thực hiện / quy trình

Quy trình tạo DTO đúng chuẩn

Bước 1:4: TạoPublish classAPI DTOlên theoAPIM quyvới tắcOAuth2 đặt tên

Ví dụ:Security

  • Import

    UserCreateRequest

    OpenAPI spec từ resources/api-definitions/
  • Enable

    UserUpdateRequest

    Security: OAuth2
  • Set

    UserResponse

    Key Manager: WSO2 IS

Bước 2:5: ThêmTest validationluồng annotationxác thực end-to-end

Bước

    3:
  • Lấy Dùngtoken DTOtừ IS
  • Gọi API qua APIM với Bearer token
  • Verify log trong controllerMI: STATUSHTTP_STATUSPAYLOAD

image.png


@PostMapping public ApiResponse<UserResponse> create( @Valid @RequestBody UserCreateRequest request) { return ApiResponse.ok(userService.create(request)); }

Quy trình logging chuẩn

Bước 1: Khai báo logger trong class


private static final Logger log = LoggerFactory.getLogger(UserService.class);

Bước 2: Dùng logger thay cho System.out.println


3.8. Ví dụDụ minhMinh họaHọa

Ví Dụ Sai — Sequence Không Có Authorization Header

Trường
<sequence hợpname="myFlow-inboundSequence">
    sai

<call>
<endpoint>
<address
uri="http://192.168.0.133:8080/api/v1/plannings"/>

</endpoint>
</call>
<respond/>
public class usercontroller { public void CreateUser() { System.out.println("Create user"); } }</sequence>

Sai vì:

  • Không

    Tênset classAuthorization: khôngBearer đúng chuẩn

    header
  • Không

    Tênlog method không camelCase

    request/response
  • Backend

    Dùngsẽ System.out.println

    reject với 401 Unauthorized
  • Không theoxử kiến trúcHTTP tầng

    status response

TrườngDụ hợpSai đúng

Hardcode
Token
Trong

Sequence
<header 
name="Authorization"
@RestControllerscope="transport" publicvalue="Bearer class UserController { private static final Logger log = LoggerFactory.getLogger(UserController.class); private final UserService userService; public UserController(UserService userService) { thiseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9..userService = userService; } @PostMapping public ApiResponse<UserResponse."/> create( @Valid @RequestBody UserCreateRequest request) { log.info("Create user {}", request.getUsername()); return ApiResponse.ok(userService.create(request)); } }

Ví dụ DTO sai


public class userDTO { public String name; }

Sai vì:

  • Token

    Tênhardcode classsẽ khônghết chuẩn

    hạn, phải sửa code mỗi lần
  • Lộ

    Khôngthông tin validation

    xác thực trong source code
  • Không

    Fieldthể public

    thay
  • Khôngđổi theo DTOmôi pattern

    trường (dev / staging / prod)

dụDụ DTOĐúng đúng

Sequence
Xác
Thực

Gọi
Backend
Chuẩn
public<sequence classname="Load_balance_example-inboundSequence">

    UserCreateRequest<!-- 1. Lưu payload gốc -->
    <property name="PAYLOAD" expression="json-eval($)" scope="default"/>
    <log level="custom">
        <property name="STATUS"  value="[KafkaConsumer] Message nhận được từ Kafka"/>
        <property name="TOPIC"   value="test_topic_01"/>
        <property name="PAYLOAD" expression="get-property('PAYLOAD')"/>
    </log>

    <!-- 2. Set Authorization header từ config -->
    <property name="BACKEND_TOKEN"
        expression="get-property('conf:backend.auth.token')" scope="default"/>
    <header name="Authorization" scope="transport"
        expression="fn:concat('Bearer ', get-property('BACKEND_TOKEN'))"/>
    <property name="Content-Type" value="application/json" scope="transport"/>

    <!-- 3. Gọi backend -->
    <call>
        <endpoint>
            <address uri="{$ctx:backend.planning.url}">
                @NotBlank<suspendOnFailure>
                    @Size(max<initialDuration>1000</initialDuration>
                    <progressionFactor>2.0</progressionFactor>
                    <maximumDuration>60000</maximumDuration>
                </suspendOnFailure>
            </address>
        </endpoint>
    </call>

    <!-- 4. Kiểm tra phản hồi -->
    <property name="HTTP_STATUS"   expression="$axis2:HTTP_SC"  scope="default"/>
    <property name="API_RESPONSE"  expression="json-eval($)"    scope="default"/>
    <log level="custom">
        <property name="STATUS"      value="[KafkaConsumer] Phản hồi Backend"/>
        <property name="HTTP_STATUS" expression="get-property('HTTP_STATUS')"/>
    </log>

    <!-- 5. Phân loại kết quả -->
    <filter xpath="get-property('HTTP_STATUS') = 255)'200' privateor String name;

    public String getName(get-property('HTTP_STATUS') { return name; }
    public void setName(String name) { this.name = name;'201'">
        }<then>
            <kafkaTransport.publishMessages configKey="KafkaConnection">
                <topic>processed_topic</topic>
            </kafkaTransport.publishMessages>
        </then>
        <else>
            <kafkaTransport.publishMessages configKey="KafkaConnection">
                <topic>error_topic</topic>
            </kafkaTransport.publishMessages>
        </else>
    </filter>

</sequence>

 

[Chỗ này thêm ảnh: WSO2 MI log viewer — hiển thị STATUS, HTTP_STATUS sau khi gọi backend thành công]

Ví Dụ Đúng — API APIM Với OAuth2 Security

# resources/api-definitions/KafkaProducerApi.yaml
openapi: 3.0.0
info:
  title: Kafka Producer API
  version: 1.0.0
paths:
  /kafka-producer:
    post:
      summary: Publish message lên Kafka
      security:
        - OAuth2: []          # Bắt buộc xác thực OAuth2 tại APIM
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Thành công
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://<IS_HOST>:9443/oauth2/token
          scopes: {}

 


3.9. Checklist ápÁp dụng

Dụng

Trước khi commit hoặc tạo PR:

Naming

  •  

    API Controller →artifact: *Controller<Name>Api.xml

  •  

    Inbound Service →Sequence: *Service<flow>-inboundSequence.xml

  •  

    Error Repository →Sequence: *Repository<flow>-inboundErrorSequence.xml

  •  

    Connection Entity →Entry: *Entity<Name>Connection.xml

  •  DTO → *Request / *Response


Token

Coding

& Xác Thực

  •  

    Không Biếnhardcode Bearer methodtoken dùngtrong camelCase

    Sequence
  •  

    Token Classlấy dùngtừ PascalCase


    config.properties hoặc local-entries
  •  

    Mọi Khônglời dùnggọi System.out.println

    backend đều có Authorization: Bearer header
  •  

    API publish lên APIM đã bật OAuth2 Security

  •  DùngKey loggingManager chuẩntrỏ slf4j

    đúng về WSO2 IS

Validation

Logging

  •  

    Sequence DTOlog STATUS, validationPAYLOAD annotation

    khi nhận request
  •  Sequence log HTTP_STATUSAPI_RESPONSE sau khi gọi backend
  •  Error Sequence log ERROR_CODEERROR_MESSAGEORIGINAL_PAYLOAD

Separation of Concerns

  •  API Layer không chứa logic điều phối phức tạp
  •  Error handling nằm trong Error Sequence riêng
  •  Kết nối Kafka trong local-entries, không khai báo lại trong Sequence
  •  URL backend lấy từ config, không hardcode

Architecture

  •  Inbound Endpoint trỏ đúng sequence và onError
  •  Luồng thành công publish vào processed_topic
  •  Luồng lỗi publish vào error_topic
  •  Không có fieldluồng publicnào trongbỏ DTO

    qua xác thực với backend

SOLID

  •  Controller không chứa nghiệp vụ

  •  Service không chứa HTTP logic

  •  Repository chỉ truy vấn DB

  •  Inject interface, không new

  •  Class có 1 trách nhiệm


Architecture

  •  Không Controller → Repository

  •  Không trả Entity ra API

  •  Có DTO mapping

  •  Exception dùng chuẩn hệ thống

3.10. Cấu trúcTrúc chuẩnChuẩn củaCủa mộtMột hàmSequence xửXử nghiệpNghiệp vụ

Vụ

3.10.1. Mục tiêu

Tiêu

Quy định cấu trúc thống nhất của một hàm xử lý nghiệp vụinboundSequence nhằm:

  • Dễ đọc và dễ review

  • Tách rõ: validatenhận dữ logicliệu logxác thực exception

    gọi backend → phân loại kết quả
  • Tránh thiếu validatelog hoặc thiếu log

    xử lý HTTP status
  • Chuẩn hóa xửcách publish lỗikết quả response

    vào
  • Kafka
  • Giúp dev mới đọc code hiểu ngay luồng xử lý

    topic

3.10.2. Cấu trúcTrúc chuẩnBắt bắt buộc

Buộc

Một hàm service/controllerinboundSequence phải theo thứ tự:

1. 
Lưu
payload

gốc
vào
property
1. Validate input 2. Log inputthông tin message nhận được 3. BusinessSet logicAuthorization 4. Catch exceptionheader (nếuBearer cần)token) 4. Gọi backend API (<call>) 5. TrảLưu HTTP status và response /6. throwLog BusinessExceptionphản hồi backend 7. Phân loại theo HTTP status (filter) - 2xx → publish vào processed_topic - 4xx → đóng gói lỗi dữ liệu → publish vào error_topic - 5xx → đóng gói lỗi backend → publish vào error_topic

 


3.10.3. Quy địnhĐịnh chiChi tiếtTiết từngTừng phần

Bước

1. Validate

Lưu
    payload:

  • <property name="PAYLOAD" expression="json-eval($)" scope="default"/>

    Validate annotation ở DTO

  • Validate nghiệp vụ ở service

Ví dụ:


if (repo.existsByUsername(request.getUsername())) { throw new BusinessException(ErrorCode.USER_EXISTS); }

2. Log
 input:

<log level="custom">
    <property name="STATUS"  value="[KafkaConsumer] Message nhận được từ Kafka"/>
    <property name="PAYLOAD" expression="get-property('PAYLOAD')"/>
</log>

Phải log:

  • input chính

  • hành động nghiệp vụ

  • lỗi


log.info("Create user {}", request.getUsername());

3. BusinessSet logic
header

Chỉxác chứathực xử(từ lý nghiệp vụ:

  • mapping

  • tính toán

  • gọi repository

  • gọi service khác


UserEntity entity = mapper.toEntity(request); repo.save(entity);

4. Exception handling

Không catch Exception chung chung nếuconfig, không xửhardcode):

lý.
<property name="BACKEND_TOKEN"
    expression="get-property('conf:backend.auth.token')"/>
<header name="Authorization" scope="transport"
    expression="fn:concat('Bearer ', get-property('BACKEND_TOKEN'))"/>

 

Sai:4. Gọi backend:

<call>
    
<endpoint>
<address

uri="{$ctx:backend.planning.url}">
<suspendOnFailure>
<initialDuration>1000</initialDuration>
try<progressionFactor>2.0</progressionFactor> {<maximumDuration>60000</maximumDuration> repo.save(entity);</suspendOnFailure> }</address> catch</endpoint> (Exception e) { } </call>

Đúng: 

5–6.

Lưu

log
phản
hồi:

try<property {name="HTTP_STATUS"  repo.save(entity);expression="$axis2:HTTP_SC"  }scope="default"/>
catch<property (DataIntegrityViolationExceptionname="API_RESPONSE" ex)expression="json-eval($)"    {scope="default"/>
log.error(<log level="DBcustom">
    error",<property ex);name="HTTP_STATUS"    throwexpression="get-property('HTTP_STATUS')"/>
    new<property BusinessException(ErrorCode.DB_ERROR);name="RESPONSE_BODY"  }expression="get-property('API_RESPONSE')"/>
</log>

5. Response

Service: 

    7.

  • Phân loại kết quả:

    <filter xpath="get-property('HTTP_STATUS') = '200' or get-property('HTTP_STATUS') = '201'">
        <then>
            <!-- publish processed_topic -->
        </then>
        <else>
            <filter xpath="get-property('HTTP_STATUS') >= '400' and get-property('HTTP_STATUS') &lt; '500'">
                <then>
                    <!-- 4xx: lỗi dữ liệu → error_topic -->
                </then>
                <else>
                    <!-- 5xx: lỗi backend → error_topic -->
                </else>
            </filter>
        </else>
    </filter>

    trả DTO

  • hoặc throw BusinessException

Controller:

  • trả ApiResponse


return UserResponse.from(entity);

Controller:


return ApiResponse.ok(userService.create(request));

3.10.4. Ví dụDụ hàmSequence sai


public User create(UserCreateRequest req) { UserEntity e = new UserEntity(); e.setUsername(req.getUsername()); repo.save(e); return e; }
Sai

Sai vì: Không có Bearer token, không log, không phân loại HTTP status, không publish kết quả.

  • Không validate

  • Không log

  • Trả entity

  • Không xử lý lỗi

  • Không theo DTO pattern


3.10.5. Checklist dụReview hàm đúng chuẩn dự án


public UserResponse create(UserCreateRequest request) { // 1. Validate if (repo.existsByUsername(request.getUsername())) { throw new BusinessException(ErrorCode.USER_EXISTS); } // 2. Log log.info("Create user {}", request.getUsername()); // 3. Business logic UserEntity entity = mapper.toEntity(request); repo.save(entity); // 4. Response return mapper.toResponse(entity); }

3.10.6. Checklist

Sequence

Khi review hàmmột service/controller:inboundSequence:

  •  

     validatelưu nghiệpPAYLOAD vụ

    vào property trước khi xử lý
  •  Có log inputthông chính

    tin nhận message
  •  

    Có set Authorization: Bearer header (từ config, không hardcode)

  •  Có log HTTP status sau khi gọi backend
  •  Có phân loại HTTP status: 2xx / 4xx / 5xx
  •  2xx → processed_topic
  •  4xx và 5xx → error_topic
  •  Không xử lý logiclỗi kỹ thuật trong controller

    sequence
  • này
  • (để

    cho KhôngError trả Entity

  •  Throw BusinessException đúng chuẩn

  •  Không catch Exception vô nghĩa

  •  Trả DTO / ApiResponse đúng chuẩn

    Sequence)

Tài liệu này thuộc phạm vi quản lý của Core Team — mọi thay đổi phải được Core Team phê duyệt.