Skip to content

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 độ Mavenvn.cake.threedsdk:cake-3ds-android:{version}
Namespacevn.cake.threedsdk
Giao thứcEMV 3DS 2.1.0, 2.2.0, 2.3.0, và 2.3.1
SDK tối thiểu21
SDK mục tiêu35
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.

groovy
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.

groovy
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.

groovy
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:

kotlin
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ể:

kotlin
val service = CakeThreeDs2ServiceImpl(
    context = applicationContext,
    sdkReferenceNumber = sdkReferenceNumber,
    enableLogging = BuildConfig.DEBUG,
    workContext = Dispatchers.IO
)
Tham sốKiểuBắt buộcMô tả
contextContextCóContext của ứng dụng
sdkReferenceNumberStringKhô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.
enableLoggingBooleanKhông (mặc định false)Bật output Logcat để debug
workContextCoroutineContextCó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ênKiểuMô tả
sdkVersionStringPhiên bản SDK hiện tại
warningsList<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:

kotlin
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ểuMô tả
sdkTransactionIdSdkTransactionIdID duy nhất cho transaction này. Gọi SdkTransactionId.create() để tạo một UUID.
directoryServerIDStringDirectory Server ID của mạng thẻ (xem bảng bên dưới)
messageVersionString?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ợ.
isLiveModeBooleanfalse sử dụng chứng chỉ Directory Server test. true sử dụng chứng chỉ production.
directoryServerNameStringTê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ý.
rootCertsList<X509Certificate>Chứng chỉ gốc của DS-CA, dùng để kiểm tra nội dung đã ký bởi ACS
dsPublicKeyPublicKeyPublic key của DS, dùng để mã hóa dữ liệu thiết bị. Lấy nó bằng service.getPublicKey(directoryServerID).
keyIdString?Định danh key tùy chọn khi DS phát hành nhiều key
uiCustomizationCakeUiCustomizationTù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ạngIDThuật toán
VisaA000000003RSA
MastercardA000000004RSA
Test (RSA)F055545342RSA
Test (EC)F155545342EC

Đố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:

kotlin
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ườngMô tả
deviceDataThông tin thiết bị, được mã hóa JWE bằng public key của DS
sdkTransactionIdSdkTransactionId mà bạn đã truyền cho createTransaction()
sdkAppIdUUID định danh cài đặt app này, được tạo bởi SDK
sdkReferenceNumberĐịnh danh tham chiếu SDK
sdkEphemeralPublicKeyPublic key EC P-256 tạm thời của SDK, dưới dạng chuỗi JWK
messageVersionPhiê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.

kotlin
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:

kotlin
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ểuMô tả
challengeParametersChallengeParametersThô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.
timeoutMinsIntThời gian timeout tổng thể cho thử thách, tối thiểu 5 phút
intentDataIntentDataDữ 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
StartchallengeViewArgs: ChallengeViewArgsVò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.
EndchallengeResult: ChallengeResultTransaction 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
SucceededuiTypeCode, transStatusChủ thẻ đã hoàn tất thử thách thành công
FaileduiTypeCodeChủ thẻ thất bại trong thử thách
CanceleduiTypeCode?Chủ thẻ đã hủy, hoặc bỏ dở thử thách
TimeoutuiTypeCode?Thử thách vượt quá timeoutMins
ProtocolErrordata: ErrorDataCRes thất bại khi kiểm tra giao thức (xem Xử lý lỗi)
RuntimeErrorthrowable: ThrowableException 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 ​

Màn hình thử thách 3DS được hiển thị bởi SDK trên Android

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:

UiTypeMãYêu cầu nút SubmitMô tả
Text01CóNhập văn bản tự do, ví dụ một mã OTP
SingleSelect02CóChọn một lựa chọn từ danh sách
MultiSelect03CóChọn nhiều lựa chọn từ danh sách
OutOfBand04Khô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ẻ
Html05KhôngHTML do ACS cung cấp, hiển thị trong một WebView
HtmlOob06KhôngThử thách HTML kết hợp với một hành động tiếp tục out-of-band
Information07KhôngMà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ườngMô tả
errorCodeMã lỗi giao thức (xem bảng bên dưới)
errorComponentComponent nào đã báo cáo lỗi: ThreeDsSdk, ThreeDsServer, DirectoryServer, hoặc Acs
errorDescriptionMô tả có thể đọc được bởi con người
errorDetailTê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ênNguyên nhân kích hoạt
101InvalidMessageReceivedTrường messageType trong CRes không phải là "CRes"
102UnsupportedMessageVersionPhiên bản bị thiếu, rỗng, hoặc không khớp với CReq
201RequiredDataElementMissingThiếu một trường bắt buộc cho UiType hiện tại
202UnrecognizedCriticalMessageExtensionsCó một critical message extension không được hỗ trợ
203InvalidDataElementFormatĐịnh dạng, độ dài, hoặc giá trị của một trường không hợp lệ
207ReservedValueRangeMột giá trị nằm trong khoảng dành riêng (reserved)
301InvalidTransactionIdID transaction trong CRes không khớp với CReq
302DataDecryptionFailureViệc giải mã JWE của CRes thất bại
402TransactionTimedoutTransaction 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.

kotlin
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.

kotlin
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ể:

kotlin
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

Developers