Cấu Trúc và Xác Thực JWT với Spring Resource Server
Phân tích cấu trúc ba phần của JWT, sự khác biệt giữa thuật toán ký HS256 và RS256, cùng cơ chế tự động thẩm định token bằng oauth2ResourceServer.
TL;DR: JSON Web Token (RFC 7519) là chuỗi gồm ba phần phân tách bằng dấu chấm header.payload.signature được mã hóa Base64URL. Trong khi phần payload chứa thông tin định danh và quyền hạn (claims), phần signature đảm bảo dữ liệu không bị sửa đổi. Thuật toán đối xứng HS256 dùng chung một khóa bí mật, phù hợp cho hệ thống monolith. Thuật toán bất đối xứng RS256 dùng cặp khóa private (để ký) và public (để kiểm tra), là tiêu chuẩn vàng cho kiến trúc microservices. Spring Security hỗ trợ cấu hình oauth2ResourceServer.jwt() để tự động thẩm định chữ ký và thời hạn mà không cần viết bộ lọc thủ công.
Cấu trúc ba phần của chuỗi JWT gồm những thành phần nào?
Một chuỗi JWT tiêu chuẩn luôn có ba phần được ngăn cách nhau bởi hai dấu chấm:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJhbGljZUBvbGh1Yi52biIsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJleHAiOjE3MTM3OTI0MDB9
.QmJC_3xNpHt8BKr5vZLd2mX9sA7cT1uY4wE6fN0oP8k
Cả ba thành phần đều được mã hóa theo chuẩn Base64URL (RFC 4648 §5):
| Thành phần | Ý nghĩa chức năng | Nội dung giải mã mẫu |
|---|---|---|
| Header | Khai báo thuật toán mã hóa và loại token | {"alg": "HS256", "typ": "JWT"} |
| Payload | Chứa các thông tin người dùng và metadata (Claims) | {"sub": "alice", "roles": ["ROLE_USER"], "exp": 1713792400} |
| Signature | Chữ ký số chứng minh tính toàn vẹn dữ liệu | Chuỗi byte nhị phân được hash từ Header và Payload |

Vì Payload có thể giải mã dễ dàng tại jwt.io, không bao giờ đặt mật khẩu, mã PIN ngân hàng hay số căn cước công dân vào bên trong token.
Các Claims chuẩn quan trọng (RFC 7519)
sub(Subject): Định danh người dùng (User ID hoặc Email).exp(Expiration): Thời điểm hết hạn (Unix timestamp) — bắt buộc phải có để tránh token tồn tại vĩnh viễn.iat(Issued At): Thời điểm phát hành token.jti(JWT ID): Mã định danh duy nhất của token, phục vụ cho cơ chế thu hồi (revocation).
Cơ chế ký số hoạt động ra sao và chữ ký bảo vệ điều gì?
Chữ ký số không có nhiệm vụ che giấu dữ liệu mà đảm bảo tính toàn vẹn (Integrity) và xác thực nguồn gốc (Authenticity).
Quy trình tạo chữ ký:
Signature = HashAlgorithm(Base64URL(Header) + "." + Base64URL(Payload), SecretKey)
Khi máy chủ nhận được token:
- Tách chuỗi thành Header, Payload và Signature.
- Dùng khóa bí mật thực hiện lại phép băm trên Header và Payload vừa nhận.
- So khớp kết quả tính toán với Signature đính kèm. Nếu kẻ xấu sửa
"roles": ["USER"]thành"roles": ["ADMIN"], phép tính lại sẽ lệch hoàn toàn và token bị từ chối ngay lập tức.
Thuật toán đối xứng HS256 phù hợp với những kịch bản nào?
Thuật toán HS256 (HMAC-SHA256) sử dụng chung một khóa bí mật (Secret Key) duy nhất cho cả hai quá trình: ký token và thẩm định chữ ký.
@Value("${app.jwt.secret}")
private String secret;
@Bean
public JwtDecoder jwtDecoder() {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return NimbusJwtDecoder.withSecretKey(key)
.macAlgorithm(MacAlgorithm.HS256)
.build();
}
- Ưu điểm: Tốc độ tính toán rất nhanh (nhanh gấp khoảng 10 lần so với RSA), cấu hình đơn giản.
- Rủi ro lớn (Blast Radius): Khóa bí mật phải được chia sẻ cho mọi dịch vụ cần kiểm tra token. Nếu một dịch vụ phụ trợ bị tấn công và rò rỉ khóa, kẻ xấu có thể tự tạo bất kỳ token nào với quyền
ADMIN. - Kịch bản sử dụng: Tối ưu cho kiến trúc nguyên khối (Monolith) nơi dịch vụ phát hành và dịch vụ kiểm tra nằm chung trong một ứng dụng.
Tại sao kiến trúc Microservices ưu tiên thuật toán bất đối xứng RS256?
Thuật toán RS256 (RSA-SHA256) sử dụng cặp khóa bất đối xứng: Private Key dùng để ký token và Public Key dùng để kiểm tra chữ ký.

// Resource Server only needs Public Key from JWKS endpoint
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder
.withJwkSetUri("https://auth.olhub.vn/.well-known/jwks.json")
.jwsAlgorithm(SignatureAlgorithm.RS256)
.build();
}
Lợi ích kiến trúc của RS256:
- Cô lập thiệt hại (Blast Radius): Private Key được bảo vệ tuyệt đối tại Auth Service. Các Microservices khác chỉ giữ Public Key, dù bị tấn công cũng không thể giả mạo token.
- Xoay vòng khóa (Key Rotation) linh hoạt: Auth Service có thể phát hành khóa mới và công khai qua URL JWKS, các service tự động cập nhật mà không cần khởi động lại đồng loạt.
Spring oauth2ResourceServer tự động xác thực token như thế nào?
Spring Security cung cấp cơ chế cấu hình nhanh chóng qua DSL oauth2ResourceServer:
@Configuration
@EnableWebSecurity
public class ResourceServerSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
}

Quy trình thẩm định tự động gồm 4 giai đoạn:
BearerTokenAuthenticationFiltertách chuỗi token từ headerAuthorization.JwtAuthenticationProvidergọiJwtDecoder.NimbusJwtDecoderđối chiếu chữ ký số và xác nhận claimexpchưa quá hạn.- Chuyển đổi dữ liệu sang
JwtAuthenticationTokennạp vàoSecurityContextHolder.
Làm thế nào để ánh xạ Claim từ JWT thành GrantedAuthority?
Mặc định Spring Security đọc quyền hạn từ claim scope với tiền tố SCOPE_. Để chuyển đổi claim tùy chỉnh roles thành ROLE_ phục vụ cho hasRole("ADMIN"), ta triển khai bộ chuyển đổi:
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
converter.setPrincipalClaimName("sub");
return converter;
}
Trong Controller, thông tin người dùng được trích xuất an toàn:
@GetMapping("/api/me")
public Map<String, Object> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"username", jwt.getSubject(),
"roles", jwt.getClaimAsStringList("roles")
);
}
Những lỗ hổng bảo mật kinh điển khi sử dụng JWT là gì?
- Tấn công Algorithm Confusion: Nếu bộ giải mã chấp nhận cả HS256 và RS256, kẻ tấn công có thể đổi thuật toán sang HS256 và ký token bằng chính Public Key của máy chủ. Khắc phục bằng cách chỉ định rõ thuật toán duy nhất:
.jwsAlgorithm(SignatureAlgorithm.RS256). - Quên đặt thời hạn Expiration (exp): Khiến token trở nên vĩnh viễn, không thể thu hồi nếu bị rò rỉ.
- Không chỉ định Charset khi băm chuỗi: Dùng
secret.getBytes()mà không thêmStandardCharsets.UTF_8sẽ dẫn đến lỗi lệch mã trên các hệ điều hành khác nhau.
Những điểm then chốt cần ghi nhớ là gì?
- JWT gồm ba phần Base64URL: Header (thuật toán), Payload (danh tính công khai), Signature (bảo vệ toàn vẹn).
- Luôn kiểm tra claim
expvà không đưa dữ liệu nhạy cảm vào Payload. - Sử dụng HS256 cho Monolith và RS256 kết hợp JWKS endpoint cho hệ thống Microservices.
oauth2ResourceServergiúp chuẩn hóa quy trình xác thực Bearer token với độ tin cậy cao.
Bạn đã nắm vững cấu trúc và cách xác thực JWT chưa?
- Q1Tại sao phần Payload của JWT lại không được xem là một cơ chế mã hóa bí mật dữ liệu?
- Q2Trong kiến trúc Microservices, tại sao RS256 lại an toàn hơn HS256 khi một service nghiệp vụ bị lộ mã nguồn?
- Q3Cuộc tấn công Algorithm Confusion (đổi từ RS256 sang HS256) khai thác điểm yếu nào của hệ thống xác thực?
- Q4Tại sao claim 'exp' (Expiration Time) lại được xem là bắt buộc phải có trên môi trường production?
- Q5Để ánh xạ claim 'roles' trong JWT thành các Authority có tiền tố 'ROLE_' phục vụ cho hasRole('ADMIN'), ta cần tùy biến component nào?
Bài tiếp theo: Issue, refresh & revoke JWT
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.
Form Login, HTTP Basic và Cơ Chế Chống Session Fixation
Bài tiếpIssue, refresh và revoke JWT — vòng đời token trong production
Lộ trình Junior Java Backend Developer
Chặng 18/21 · Authentication & Authorization — bài 10/19 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này