Case study, super app
Xác thực bổ sung mà luật chơi nằm trong config
Mình phụ trách challenge-chain service, chỗ quyết định khách phải chứng minh những gì trước khi làm một việc nhạy cảm. Đây là cách nó được ráp lại, và nó giúp team nhẹ gánh ở đâu.
Bài toán
Trong super app, có việc chỉ đăng nhập thôi là chưa đủ: chuyển tiền, đăng nhập trên máy mới, đặt PIN. Mỗi việc cần một bộ kiểm tra riêng, và luật thì đổi liên tục. Nếu luật nằm trong code dưới dạng if-else, đổi một luật là sửa code rồi release.
Nó nằm ở đâu
BFF biết vì sao cần kiểm tra nên nó mở chain cho hành động đó. App trả lời từng challenge qua gateway. Xong chain, service thực hiện hành động hỏi lại challenge-chain service xem phiên đã đạt yêu cầu chưa, đạt rồi mới làm.
Chain là dữ liệu
Mỗi hành động gắn với một chain. Chain nói khách phải chứng minh điều gì, còn engine tự lên kế hoạch các bước để đáp ứng. Chain và action lưu dạng JSONB, được đưa qua các môi trường bằng Liquibase. Version chỉ được thêm, không bao giờ sửa, nên lần thử nào cũng đọc lại được theo đúng version nó đã chạy.
Ví dụ, đăng nhập trên máy mới có thể đòi PIN, selfie, ảnh giấy tờ và quét chip NFC. Chuyển tiền vượt ngưỡng thì đòi thêm selfie ngoài OTP.
Mức đảm bảo và mức sàn
Mỗi bước chứng minh được tính vào một loại bằng chứng, cộng lại thành mức đảm bảo. Mỗi hạng khách có một mức sàn trong config mà hành động phải đạt. Đặt PIN hay đồng ý điều khoản thì không cộng điểm nào, vì đăng ký credential chẳng nói lên được ai đang cầm điện thoại.
| Challenge | Tính là bằng chứng gì |
|---|---|
| Mật khẩu hoặc PIN | Thứ mình biết |
| OTP | Thứ mình có |
| Selfie | Sinh trắc học |
| Ảnh giấy tờ | Giấy tờ, ảnh chụp |
| Quét chip NFC | Giấy tờ, chip |
Luật trước khi version lên sóng
Version chain mới nào cũng phải qua validator mới được dùng. Vài luật tiêu biểu:
- bước nào cũng phải là loại challenge mà engine hỗ trợ
- chain của một hành động phải đạt mức sàn của hạng khách
- version mới không được hạ mức mà hành động đang có
- caller không được gửi những attribute chỉ hệ thống mới được ghi
Mọi config đều đi qua đúng một validator này, nên nguồn config thêm vào sau cũng không lách được.
Bên trong service
Service có ba module. core chứa engine, planner, validator và luật đảm bảo, ArchUnit đánh fail build nếu nó dám import Spring Web, JPA, Redis, Kafka hay SDK của vendor. api là module duy nhất biết HTTP. infra giữ mỗi loại challenge một adapter, cộng với các store. Danh mục challenge được dựng từ các adapter đã đăng ký lúc khởi động, hai adapter giành cùng một loại là service không chịu boot.
Phiên nằm trong Redis có TTL. Config và lịch sử lần thử nằm trong PostgreSQL. Lúc bắt đầu một lần thử được ghi đồng bộ trước khi OTP bị dùng, nên Redis có mất thì dấu vết khách đã dùng gì vẫn còn.
core, và ArchUnit canh giữ chuyện đó.Giữ đường nóng nhẹ nhàng
Profiling cho thấy mỗi lần start chain tốn ba query, mỗi lần switch tốn năm. Mình đặt Caffeine cache trước phần đọc config trên đường nóng, dữ liệu giữa các pod chỉ cũ tối đa bằng TTL, và cache được xoá tại chỗ khi có config mới thêm vào.
Được gì
- Thêm chain mới hay đổi ngưỡng chỉ là đổi config, đi qua các môi trường như mọi migration khác.
- Thêm một kiểu challenge mới là thêm một adapter, engine giữ nguyên.
- Mức đảm bảo được tính ra, nên chain không thể khai nhiều hơn những gì các bước chứng minh.
- Đổi vendor thì chỉ đụng tới
infra.
- Java 25
- Spring Boot 4
- PostgreSQL
- Redis
- Caffeine
- Liquibase
- ArchUnit
Chi tiết đã được đơn giản hoá và không nêu tên khách hàng.