Xác thực 3DS — Android
Cake 3DS SDK cho Android (vn.cake.threedsdk) triển khai giao thức EMV 3-D Secure 2.x cho việc xác thực chủ thẻ. SDK thu thập và mã hóa dữ liệu thiết bị. Nó trao đổi các thông điệp thử thách CReq và CRes. Nó hiển thị giao diện thử thách. Nó xử lý các lỗi giao thức. Đặc tả EMVCo yêu cầu SDK giao tiếp trực tiếp với Directory Server (DS) và Access Control Server (ACS).
Chi tiết SDK
| Tọa độ Maven | vn.cake.threedsdk:cake-3ds-android:{version} |
| Namespace | vn.cake.threedsdk |
| Giao thức | EMV 3DS 2.1.0, 2.2.0, 2.3.0, và 2.3.1 |
| SDK tối thiểu | 21 |
| SDK mục tiêu | 35 |
| Ngôn ngữ | Kotlin (Java 11), Kotlin Coroutines cho các lệnh gọi bất đồng bộ |
Cài đặt
SDK hỗ trợ hai phương thức cài đặt: một Maven repository cục bộ, hoặc một file AAR trực tiếp.
Phương án 1: Maven repository cục bộ (khuyến nghị)
Bước 1. Thêm Maven repository từ Cake vào file build.gradle gốc của app.
allprojects {
repositories {
maven { url '/path/to/cake-3ds-maven-repo' }
}
}Bước 2. Thêm dependency vào file build.gradle của module app.
dependencies {
implementation 'vn.cake.threedsdk:cake-3ds-android:{version}'
}Phương án 2: File AAR trực tiếp
Bước 1. Sao chép file AAR từ Cake vào thư mục libs/ của app.
Bước 2. Thêm file AAR và các dependency bắc cầu (transitive) của nó vào file build.gradle của module app.
dependencies {
implementation files('libs/3ds2sdk-release.aar')
// Các dependency bắc cầu (transitive) — thêm cả những dòng này
implementation 'com.nimbusds:nimbus-jose-jwt:10.4.2'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.3'
implementation 'androidx.fragment:fragment-ktx:1.8.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1'
}Khởi tạo SDK
Tạo một instance CakeThreeDs2ServiceImpl cho mỗi phiên app. Tạo nó trong Application.onCreate(), hoặc trong một module dependency injection. SDK cung cấp hai constructor công khai.
Constructor mặc định sử dụng một số tham chiếu SDK tích hợp sẵn:
import vn.cake.threedsdk.service.CakeThreeDs2ServiceImpl
import kotlinx.coroutines.Dispatchers
val service = CakeThreeDs2ServiceImpl(
context = applicationContext,
enableLogging = BuildConfig.DEBUG,
workContext = Dispatchers.IO
)Sử dụng constructor thứ hai khi Cake cung cấp cho bạn một sdkReferenceNumber cụ thể:
val service = CakeThreeDs2ServiceImpl(
context = applicationContext,
sdkReferenceNumber = sdkReferenceNumber,
enableLogging = BuildConfig.DEBUG,
workContext = Dispatchers.IO
)| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
context | Context | Có | Context của ứng dụng |
sdkReferenceNumber | String | Không | Định danh tham chiếu SDK. Bỏ qua để dùng giá trị mặc định tích hợp sẵn của SDK. |
enableLogging | Boolean | Không (mặc định false) | Bật output Logcat để debug |
workContext | CoroutineContext | Có | Dispatcher dùng cho các tác vụ SDK chạy nền (thu thập dữ liệu thiết bị, mã hóa) |
| Thành viên | Kiểu | Mô tả |
|---|---|---|
sdkVersion | String | Phiên bản SDK hiện tại |
warnings | List<Warning> | Các cảnh báo phát hiện được trong quá trình khởi tạo, ví dụ thiết bị đã root hoặc có debugger đính kèm |
Khi phiên app kết thúc, gọi service.cleanup() một lần để giải phóng tài nguyên SDK.
Tạo một transaction
Với mỗi thanh toán yêu cầu xác thực 3DS, tạo một Transaction:
val transaction = service.createTransaction(
sdkTransactionId = SdkTransactionId.create(),
directoryServerID = directoryServerId, // ví dụ "A000000003" cho Visa
messageVersion = null, // null = SDK tự chọn phiên bản cao nhất được hỗ trợ
isLiveMode = !BuildConfig.DEBUG,
directoryServerName = "visa",
rootCerts = directoryServerRootCerts,
dsPublicKey = directoryServerPublicKey,
keyId = null,
uiCustomization = CakeUiCustomization.createWithAppTheme(activity)
)| Tham số | Kiểu | Mô tả |
|---|---|---|
sdkTransactionId | SdkTransactionId | ID duy nhất cho transaction này. Gọi SdkTransactionId.create() để tạo một UUID. |
directoryServerID | String | Directory Server ID của mạng thẻ (xem bảng bên dưới) |
messageVersion | String? | Phiên bản giao thức cần dùng. Truyền null để dùng phiên bản mới nhất mà SDK hỗ trợ. |
isLiveMode | Boolean | false sử dụng chứng chỉ Directory Server test. true sử dụng chứng chỉ production. |
directoryServerName | String | Tên dễ đọc của DS. SDK dùng nó để tìm logo DS hiển thị trên màn hình xử lý. |
rootCerts | List<X509Certificate> | Chứng chỉ gốc của DS-CA, dùng để kiểm tra nội dung đã ký bởi ACS |
dsPublicKey | PublicKey | Public key của DS, dùng để mã hóa dữ liệu thiết bị. Lấy nó bằng service.getPublicKey(directoryServerID). |
keyId | String? | Định danh key tùy chọn khi DS phát hành nhiều key |
uiCustomization | CakeUiCustomization | Tùy chỉnh giao diện cho màn hình thử thách (xem Tùy chỉnh giao diện thử thách) |
Directory Server
service.getPublicKey(directoryServerID) phân giải các mạng sau:
| Mạng | ID | Thuật toán |
|---|---|---|
| Visa | A000000003 | RSA |
| Mastercard | A000000004 | RSA |
| Test (RSA) | F055545342 | RSA |
| Test (EC) | F155545342 | EC |
Đối với một mạng ngoài danh sách này, hãy tự cung cấp dsPublicKey và rootCerts của riêng bạn cho createTransaction(...) thay vì dùng service.getPublicKey(...).
Lấy tham số authentication request
Trước khi gửi Authentication Request (AReq) đến 3DS Server của bạn, gọi createAuthenticationRequestParameters() cho mỗi transaction:
lifecycleScope.launch {
val params = transaction.createAuthenticationRequestParameters()
// Gửi `params` đến backend của bạn để xây dựng và gửi AReq
}Đây là một hàm suspend. Hãy gọi nó từ một coroutine. Nó thu thập và mã hóa dữ liệu thiết bị. Nó cũng tạo cặp khóa tạm thời (ephemeral) cho kênh bảo mật ACS.
| Trường | Mô tả |
|---|---|
deviceData | Thông tin thiết bị, được mã hóa JWE bằng public key của DS |
sdkTransactionId | SdkTransactionId mà bạn đã truyền cho createTransaction() |
sdkAppId | UUID định danh cài đặt app này, được tạo bởi SDK |
sdkReferenceNumber | Định danh tham chiếu SDK |
sdkEphemeralPublicKey | Public key EC P-256 tạm thời của SDK, dưới dạng chuỗi JWK |
messageVersion | Phiên bản giao thức được chọn cho transaction này |
Backend của bạn đưa các trường này vào AReq. Response từ 3DS Server, ARes, cho biết kết quả. Một kết quả frictionless không cần thêm hành động nào từ SDK. Ngược lại, ARes yêu cầu luồng thử thách.
Khởi chạy thử thách
Đăng ký ChallengeContract trước khi activity hoặc fragment đạt trạng thái STARTED, ví dụ như một field được đặt trong onCreate. Android yêu cầu việc đăng ký sớm này cho mọi ActivityResultContract.
import vn.cake.threedsdk.transaction.ChallengeContract
import vn.cake.threedsdk.transaction.ChallengeResult
val challengeLauncher = registerForActivityResult(ChallengeContract()) { result: ChallengeResult ->
handleChallengeResult(result)
}
fun handleChallengeResult(result: ChallengeResult) {
when (result) {
is ChallengeResult.Succeeded -> onAuthenticationSucceeded(result)
is ChallengeResult.Failed -> onAuthenticationFailed(result)
is ChallengeResult.Canceled -> onAuthenticationCanceled(result)
is ChallengeResult.Timeout -> onAuthenticationTimedOut(result)
is ChallengeResult.ProtocolError -> onProtocolError(result.data)
is ChallengeResult.RuntimeError -> onRuntimeError(result.throwable)
}
}Khi ARes yêu cầu một thử thách, xây dựng ChallengeParameters từ nội dung của nó. Sau đó gửi các tham số này đến một InitChallengeRepository. Repository chạy trao đổi CReq và CRes ban đầu trước khi SDK hiển thị bất kỳ giao diện nào. Khi thành công, truyền kết quả đến challengeLauncher mà bạn đã đăng ký ở trên:
import vn.cake.threedsdk.transaction.ChallengeParameters
import vn.cake.threedsdk.transaction.IntentData
import vn.cake.threedsdk.transaction.InitChallengeRepositoryFactory
import vn.cake.threedsdk.transaction.InitChallengeResult
val challengeParameters = ChallengeParameters(
threeDsServerTransactionId = ares.threeDsServerTransId,
acsTransactionId = ares.acsTransId,
acsRefNumber = ares.acsReferenceNumber,
acsSignedContent = ares.acsSignedContent,
dsTransactionId = ares.dsTransId
)
val initArgs = transaction.createInitChallengeArgs(
challengeParameters = challengeParameters,
timeoutMins = 5,
intentData = IntentData(
clientSecret = clientSecret,
sourceId = sourceId,
publishableKey = publishableKey
)
)
val repository = InitChallengeRepositoryFactory(
application = application,
isLiveMode = !BuildConfig.DEBUG,
sdkTransactionId = transaction.sdkTransactionId,
uiCustomization = uiCustomization,
rootCerts = directoryServerRootCerts,
enableLogging = BuildConfig.DEBUG,
workContext = Dispatchers.IO
).create()
lifecycleScope.launch {
when (val result = repository.startChallenge(initArgs)) {
is InitChallengeResult.Start ->
// CReq và CRes ban đầu thành công, khởi chạy giao diện thử thách
challengeLauncher.launch(result.challengeViewArgs)
is InitChallengeResult.End ->
// Không cần giao diện, ví dụ request ban đầu đã thất bại hoặc timeout
handleChallengeResult(result.challengeResult)
}
}| Tham số | Kiểu | Mô tả |
|---|---|---|
challengeParameters | ChallengeParameters | Thông tin ACS từ ARes. 3DS Server transaction ID, ACS transaction ID, ACS reference number, và nội dung đã ký bởi ACS đều là bắt buộc. |
timeoutMins | Int | Thời gian timeout tổng thể cho thử thách, tối thiểu 5 phút |
intentData | IntentData | Dữ liệu mờ (opaque) (client secret, source ID, publishable key) mà SDK trả lại cho bạn không đổi trong ChallengeResult |
repository.startChallenge(initArgs) xác thực nội dung đã ký bởi ACS dựa trên các chứng chỉ gốc DS-CA. Nó hoàn tất trao đổi khóa Diffie-Hellman (ECDH P-256). SDK sử dụng trao đổi này để mã hóa và giải mã các thông điệp CReq và CRes tiếp theo. repository.startChallenge(initArgs) cũng gửi CReq đầu tiên. Kết quả là một InitChallengeResult dạng sealed:
| Biến thể | Trường | Ý nghĩa |
|---|---|---|
Start | challengeViewArgs: ChallengeViewArgs | Vòng trao đổi ACS đầu tiên thành công. Truyền giá trị này cho challengeLauncher.launch(...) để hiển thị giao diện thử thách. |
End | challengeResult: ChallengeResult | Transaction kết thúc trước khi SDK hiển thị bất kỳ giao diện nào, ví dụ với ProtocolError, Timeout, hoặc RuntimeError trên request ban đầu. Xử lý nó giống như một kết quả ChallengeContract. |
Màn hình xử lý
Gọi transaction.getProgressView(activity) để lấy một dialog không thể hủy với logo DS và một chỉ báo tiến trình. Hiển thị dialog này sau khi bạn gửi AReq. Ẩn nó khi giao diện thử thách xuất hiện. Đặc tả EMVCo yêu cầu Màn Hình Xử Lý này.
ChallengeResult
Callback của ActivityResultContract trả về lớp sealed này:
| Biến thể | Trường | Ý nghĩa |
|---|---|---|
Succeeded | uiTypeCode, transStatus | Chủ thẻ đã hoàn tất thử thách thành công |
Failed | uiTypeCode | Chủ thẻ thất bại trong thử thách |
Canceled | uiTypeCode? | Chủ thẻ đã hủy, hoặc bỏ dở thử thách |
Timeout | uiTypeCode? | Thử thách vượt quá timeoutMins |
ProtocolError | data: ErrorData | CRes thất bại khi kiểm tra giao thức (xem Xử lý lỗi) |
RuntimeError | throwable: Throwable | Exception không mong đợi, ví dụ lỗi mạng hoặc lỗi phân tích dữ liệu |
Mỗi biến thể mang theo intentData, giá trị IntentData mà bạn đã truyền vào. Mỗi biến thể cũng mang theo initialUiType, UiType của màn hình thử thách đầu tiên được hiển thị. Sử dụng các trường này để khớp kết quả với luồng thanh toán của bạn. Bạn không cần giữ trạng thái riêng.
Màn hình thử thách

ChallengeActivity hiển thị màn hình này. CakeUiCustomization mà bạn truyền cho createTransaction(...) kiểm soát màu sắc, font chữ, và kiểu nút của nó. Xem Tùy chỉnh giao diện thử thách để biết toàn bộ các tùy chọn.
Luồng frictionless so với luồng thử thách
stateDiagram-v2
[*] --> Created: createTransaction()
Created --> AuthParams: createAuthenticationRequestParameters()
AuthParams --> Frictionless: ARes, no challenge required
AuthParams --> Challenge: ARes, challenge required
Challenge --> Completed: ChallengeResult Succeeded or Failed
Challenge --> Error: ChallengeResult ProtocolError or RuntimeError
Challenge --> TimedOut: ChallengeResult Timeout
Frictionless --> [*]
Completed --> [*]
Error --> [*]
TimedOut --> [*]
Các kiểu giao diện thử thách
ACS quyết định giao diện nào màn hình thử thách sẽ hiển thị. SDK expose giá trị này qua initialUiType, và cũng bên trong ErrorData:
UiType | Mã | Yêu cầu nút Submit | Mô tả |
|---|---|---|---|
Text | 01 | Có | Nhập văn bản tự do, ví dụ một mã OTP |
SingleSelect | 02 | Có | Chọn một lựa chọn từ danh sách |
MultiSelect | 03 | Có | Chọn nhiều lựa chọn từ danh sách |
OutOfBand | 04 | Không | Được giải quyết bên ngoài app, ví dụ trong ứng dụng ngân hàng của chủ thẻ |
Html | 05 | Không | HTML do ACS cung cấp, hiển thị trong một WebView |
HtmlOob | 06 | Không | Thử thách HTML kết hợp với một hành động tiếp tục out-of-band |
Information | 07 | Không | Màn hình thông tin với một nút tiếp tục, không có nhập liệu |
Xử lý lỗi
ChallengeResult.ProtocolError mang theo một đối tượng ErrorData. Đối tượng này mô tả lý do CRes thất bại khi kiểm tra hợp lệ:
| Trường | Mô tả |
|---|---|
errorCode | Mã lỗi giao thức (xem bảng bên dưới) |
errorComponent | Component nào đã báo cáo lỗi: ThreeDsSdk, ThreeDsServer, DirectoryServer, hoặc Acs |
errorDescription | Mô tả có thể đọc được bởi con người |
errorDetail | Tên trường cụ thể thất bại khi kiểm tra hợp lệ, nếu có |
Mã lỗi giao thức
| Mã | Tên | Nguyên nhân kích hoạt |
|---|---|---|
101 | InvalidMessageReceived | Trường messageType trong CRes không phải là "CRes" |
102 | UnsupportedMessageVersion | Phiên bản bị thiếu, rỗng, hoặc không khớp với CReq |
201 | RequiredDataElementMissing | Thiếu một trường bắt buộc cho UiType hiện tại |
202 | UnrecognizedCriticalMessageExtensions | Có một critical message extension không được hỗ trợ |
203 | InvalidDataElementFormat | Định dạng, độ dài, hoặc giá trị của một trường không hợp lệ |
207 | ReservedValueRange | Một giá trị nằm trong khoảng dành riêng (reserved) |
301 | InvalidTransactionId | ID transaction trong CRes không khớp với CReq |
302 | DataDecryptionFailure | Việc giải mã JWE của CRes thất bại |
402 | TransactionTimedout | Transaction vượt quá thời gian timeout đã cấu hình |
SDK tự động thông báo lỗi cho ACS. Đây là một hành động fire-and-forget. SDK trả về ChallengeResult.ProtocolError cho app của bạn sau đó. Bạn không cần thực hiện thêm hành động nào khác.
Tùy chỉnh giao diện thử thách
Bạn có thể tạo một CakeUiCustomization theo hai cách.
Phương án 1: Từ theme của app
CakeUiCustomization.createWithAppTheme(activity) đọc màu chính (primary), màu nhấn (accent), và màu chữ từ theme của app bạn. Nó áp dụng các màu này cho toolbar, label, và các nút mặc định.
val uiCustomization = CakeUiCustomization.createWithAppTheme(activity)Sử dụng tùy chọn này khi màn hình thử thách cần tự động khớp với phần còn lại của app. Bạn vẫn có thể ghi đè từng phần riêng lẻ sau đó, như ví dụ bên dưới cho thấy.
Phương án 2: Tùy chỉnh thủ công
Constructor CakeUiCustomization() đơn giản bắt đầu mà không có giá trị mặc định nào. Bạn tự đặt mọi màu sắc và kiểu dáng.
val uiCustomization = CakeUiCustomization()Sử dụng tùy chọn này để có toàn quyền kiểm soát màn hình thử thách.
Tùy chỉnh từng phần
CakeUiCustomization chứa bốn đối tượng tùy chỉnh: một cho toolbar, một cho label, một cho ô nhập liệu, và một bộ nút, mỗi bộ cho một ButtonType. Mỗi đối tượng kiểm soát một phần khác nhau của màn hình thử thách. Ví dụ bên dưới bắt đầu từ createWithAppTheme(activity) (Phương án 1) và ghi đè các phần cụ thể:
import vn.cake.threedsdk.init.ui.CakeUiCustomization
import vn.cake.threedsdk.init.ui.CakeToolbarCustomization
import vn.cake.threedsdk.init.ui.CakeLabelCustomization
import vn.cake.threedsdk.init.ui.CakeTextBoxCustomization
import vn.cake.threedsdk.init.ui.CakeButtonCustomization
import vn.cake.threedsdk.init.ui.UiCustomization.ButtonType
val uiCustomization = CakeUiCustomization.createWithAppTheme(activity).apply {
setAccentColor("#FF6B00")
val toolbar = CakeToolbarCustomization().apply {
setBackgroundColor("#1A1A1A")
setStatusBarColor("#000000")
setHeaderText("Secure Checkout")
setButtonText("CANCEL")
setTextColor("#FFFFFF")
}
setToolbarCustomization(toolbar)
val labels = CakeLabelCustomization().apply {
setHeadingTextColor("#000000")
setHeadingTextFontSize(20)
setTextColor("#333333")
setTextFontSize(16)
}
setLabelCustomization(labels)
val textBox = CakeTextBoxCustomization().apply {
setBorderColor("#89000000")
setBorderWidth(2)
setCornerRadius(8)
setHintTextColor("#89000000")
}
setTextBoxCustomization(textBox)
val submitButton = CakeButtonCustomization().apply {
setBackgroundColor("#FF6B00")
setTextColor("#FFFFFF")
setCornerRadius(8)
}
setButtonCustomization(submitButton, ButtonType.SUBMIT)
setButtonCustomization(submitButton, ButtonType.NEXT)
setButtonCustomization(submitButton, ButtonType.CONTINUE)
val cancelButton = CakeButtonCustomization().apply {
setBackgroundColor("#EC4847")
setTextColor("#FFFFFF")
}
setButtonCustomization(cancelButton, ButtonType.CANCEL)
}Lưu ý về import
ButtonType nằm lồng bên trong interface UiCustomization. Import nó với import vn.cake.threedsdk.init.ui.UiCustomization.ButtonType.
CakeToolbarCustomization
| Phương thức | Đặt |
|---|---|
setBackgroundColor(hex) | Màu nền của toolbar |
setStatusBarColor(hex) | Màu của status bar phía trên toolbar |
setHeaderText(text) | Văn bản tiêu đề hiển thị trong toolbar |
setButtonText(text) | Văn bản của nút hủy trong toolbar |
setTextColor(hex) | Màu của văn bản tiêu đề và văn bản nút |
CakeLabelCustomization
| Phương thức | Đặt |
|---|---|
setHeadingTextColor(hex) | Màu của văn bản tiêu đề |
setHeadingTextFontName(name) | Font của văn bản tiêu đề |
setHeadingTextFontSize(size) | Cỡ chữ của văn bản tiêu đề |
setTextColor(hex) | Màu của văn bản label chính |
setTextFontName(name) | Font của văn bản label chính |
setTextFontSize(size) | Cỡ chữ của văn bản label chính |
CakeTextBoxCustomization
| Phương thức | Đặt |
|---|---|
setBorderColor(hex) | Màu viền của ô nhập liệu |
setBorderWidth(dp) | Độ dày viền của ô nhập liệu |
setCornerRadius(dp) | Bán kính bo góc của ô nhập liệu |
setHintTextColor(hex) | Màu của văn bản gợi ý (hint) |
setTextColor(hex) | Màu của văn bản mà chủ thẻ nhập |
setTextFontName(name) | Font của văn bản mà chủ thẻ nhập |
setTextFontSize(size) | Cỡ chữ của văn bản mà chủ thẻ nhập |
CakeButtonCustomization
| Phương thức | Đặt |
|---|---|
setBackgroundColor(hex) | Màu nền của nút |
setCornerRadius(dp) | Bán kính bo góc của nút |
setTextColor(hex) | Màu của văn bản nút |
setTextFontName(name) | Font của văn bản nút |
setTextFontSize(size) | Cỡ chữ của văn bản nút |
Đặt một CakeButtonCustomization cho mỗi nút bạn muốn tùy chỉnh. ButtonType định nghĩa bảy giá trị: SUBMIT, CONTINUE, NEXT, CANCEL, RESEND, SELECT, và ADD_CHOICE. Đối với một loại nút ngoài danh sách này, gọi setButtonCustomization(customization, "your_button_type") với một chuỗi thay thế.
Truyền uiCustomization kết quả vào service.createTransaction(...).
Sơ đồ luồng đầy đủ
sequenceDiagram
participant App as Merchant App
participant SDK as 3DS SDK
participant ACS as Issuer ACS
App->>SDK: CakeThreeDs2ServiceImpl(context, enableLogging, workContext)
SDK-->>App: CakeThreeDs2Service
App->>SDK: createTransaction(directoryServerID, dsPublicKey, ...)
SDK-->>App: Transaction
App->>SDK: createAuthenticationRequestParameters()
SDK-->>App: AuthenticationRequestParameters
Note over App: Send AReq to your 3DS Server, receive ARes
alt Frictionless
Note over App: ARes indicates no challenge is needed
else Challenge required
App->>SDK: createInitChallengeArgs(challengeParameters, timeoutMins, intentData)
SDK-->>App: InitChallengeArgs
App->>SDK: startChallenge(initArgs)
SDK->>ACS: Validate ACS-signed content, key exchange, initial CReq
ACS-->>SDK: Initial CRes
SDK-->>App: InitChallengeResult
alt Start
App->>SDK: launch(challengeViewArgs)
SDK->>ACS: Remaining CReq and CRes exchange
ACS-->>SDK: Challenge result
SDK-->>App: ChallengeResult
else End
Note over App: Transaction ended before any UI, for example ProtocolError, Timeout, or RuntimeError
end
end