← All posts

Mọi lần xác thực token đều dội về máy chủ gốc của chúng tôi. Giờ JWKS đã sống trên biên.

Authagonal·July 20, 2026
authoidcjwkscdncloudflarecachingkey-rotation

Để xác thực một trong các JWT của chúng tôi, một dịch vụ tải về hai tài liệu công khai từ issuer: tài liệu discovery tại /.well-known/openid-configuration, và JWKS mà nó trỏ tới — tập khóa công khai dùng để ký các token. Đây là những thứ được yêu cầu nhiều nhất và ít bí mật nhất mà chúng tôi phục vụ. Chúng là những khóa công khai, vốn dĩ đã công khai theo định nghĩa, và siêu dữ liệu chỉ thay đổi khi chúng tôi xoay khóa. Vậy mà cho tới gần đây, từng lần tải một trong số đó đều phải chạy suốt quãng đường về tận máy chủ gốc của chúng tôi. Vì chúng tôi vận hành đa tenant, chuyện còn tệ hơn cả những gì câu vừa rồi nghe có vẻ vậy.

Máy chủ gốc đang làm việc mà nó chẳng có lý do gì để làm

Mỗi tenant là một issuer riêng, với tài liệu .well-known riêng và JWKS riêng của nó. Và mọi bên tin cậy, mọi máy chủ tài nguyên, mọi SDK xác thực một token đều kéo về các tài liệu đó — lúc khởi động nguội, khi bản cache ngắn ngủi của chính nó hết hạn, mỗi instance dịch vụ một lần. Nhân số issuer trên mỗi tenant với số bộ xác thực trên mỗi dịch vụ rồi nhân với số lần cache trượt, bạn sẽ có một vòi rồng phun đầy yêu cầu, tất cả đều đổ dồn về ứng dụng, tất cả chỉ để lấy những tài liệu giống hệt nhau tới từng byte đối với mọi bên gọi và chỉ thay đổi khi chúng tôi xoay một khóa.

Đó chính là chân dung sách giáo khoa của một thứ đáng lẽ nên được cache: công khai, giống hệt nhau, hiếm khi thay đổi. Thế mà ứng dụng lại đang dựng ra các khóa công khai, một cách động, ngay trên đường đi nóng của việc xác thực token của tất cả những người khác. Các khóa đó chẳng có lý do gì phải được tính toán lại theo từng yêu cầu.

Đưa chúng ra biên

Bản thân thay đổi thì nhỏ: đặt các header Cache-Control trung thực lên các phản hồi discovery và JWKS để Cloudflare cache chúng ở biên. Giờ đây một bộ xác thực chạm tới điểm hiện diện Cloudflare gần nhất, và máy chủ gốc chỉ phục vụ mỗi tài liệu khoảng một lần trên mỗi PoP trong mỗi TTL, thay vì một lần cho mỗi lần xác thực. Discovery thì cache dễ như bỡn — nó gần như không bao giờ thay đổi. JWKS mới là thứ bạn phải nghĩ cho kỹ, bởi JWKS là tài liệu duy nhất mà toàn bộ nhiệm vụ của nó là thay đổi đúng vào lúc bạn xoay một khóa, và việc cache một thứ buộc phải tươi mới ngay tại khoảnh khắc duy nhất nó thay đổi chính là chỗ người ta tự làm mình đau.

Một JWKS bị cache cắn bạn như thế nào

Xoay khóa cộng với một tập khóa đã bị cache là một cái bẫy, và nó hỏng theo hướng tồi tệ nhất có thể. Bạn xoay sang một khóa ký mới, bắt đầu phát hành token bằng nó, và một bộ xác thực đã tải JWKS trước khi khóa mới xuất hiện đang giữ một bản sao cũ. Một token đến, mang theo key id của khóa mới; bộ xác thực tra cứu nó, không tìm thấy, và từ chối token. Token đó hoàn toàn hợp lệ. Chữ ký là thật. Người dùng chẳng làm gì sai. Chính bản cache ở biên của bạn vừa trả về một 401 cho một lượt đăng nhập hợp lệ.

Hãy để ý nó hỏng theo chiều nào. Một khóa riêng tư cũ là chuyện chẳng có gì — đơn giản là bạn chưa bắt đầu dùng khóa mới thôi. Một tập khóa công khai cũ lại là một sự cố tự chuốc lấy, và cái bản cache gây ra nó là do chính bạn cố ý đưa vào, để tiết kiệm lưu lượng về máy chủ gốc. Sự an toàn của toàn bộ lược đồ sống hay chết đều nằm ở việc xoay khóa.

Khuôn mẫu an toàn khi xoay khóa

Sự an toàn nằm hoàn toàn ở thứ tự, và thứ tự đó ngược hẳn với một cú tráo nguyên tử. Ba quy tắc:

  • Công bố trước khi ký. Khóa mới vào JWKS trước, và bạn không ký lấy một token nào bằng nó cho tới khi JWKS đó — kèm cả key id mới — đã sống ở biên ít nhất trọn một TTL cache. Đến lúc bất kỳ token nào mang khóa mới có thể chạm tới một bộ xác thực, thì tập khóa đã cache của bộ xác thực đã chứa sẵn khóa đó rồi.
  • Cho nghỉ hưu sau khi hết hạn, không phải lúc xoay khóa. Khóa cũ vẫn được công bố trong JWKS cho tới khi token cuối cùng mà nó từng ký đã hết hạn. Cũ và mới cùng tồn tại trong suốt quãng chồng lấn. Rút khóa cũ ngay khoảnh khắc bạn xoay và bạn sẽ bỏ rơi mọi token nó đã ký mà vẫn còn hợp lệ.
  • TTL ngắn hơn quãng chồng lấn. Giá trị max-age của cache ở biên, cộng thêm một khoảng dư cho sai lệch đồng hồ, phải ngắn hơn cửa sổ chồng lấn của khóa. Cache được phép cũ; nhưng nó không bao giờ được phép cũ lâu hơn cửa sổ mà cả hai khóa đều còn tốt. Chính bất đẳng thức duy nhất đó là thứ khiến một JWKS bị cache một cách quyết liệt vẫn an toàn.

Ghép lại, việc xoay khóa trở thành một pha hòa lẫn chuyển tiếp với một trình tự cố định: thêm khóa mới, chờ một TTL để biên và mọi bộ xác thực trông thấy nó, bắt đầu ký bằng nó, chờ các token cũ già đi rồi hết hạn, rồi mới bỏ khóa cũ. Mỗi bước đều an toàn trước một bản cache chậm sau một TTL, bởi vì TTL đã được chọn nhỏ hơn mọi cửa sổ mà một bản cache có thể bị chậm sau.

Bài học, đúc kết lại

Cache thường là một quyết định về độ trễ. Cache khóa công khai lại là một quyết định về tính đúng đắn, bởi thứ bạn đang cache chính là thứ quyết định token nào là thật. Bạn hoàn toàn có thể đưa nó ra biên — bạn nên làm vậy, vì nó công khai và nó nóng — nhưng chỉ khi việc xoay khóa đã được diễn đạt thành một quãng chồng lấn có thứ tự thay vì một cú tráo, và chỉ khi TTL cache đã chứng minh được là ngắn hơn quãng chồng lấn ấy. Làm đúng điều đó thì những tài liệu bận rộn nhất, dễ cache nhất trong hệ thống xác thực của bạn sẽ thôi chạm tới máy chủ gốc, và chẳng ai nhận ra sự khác biệt bao giờ — mà đó mới chính là điều cốt yếu.