简介

FIDO2提供基于WebAuthn标准的FIDO2 线上快速身份验证客户端实现,为应用及浏览器提供安卓Java API,支持使用USB、NFC、蓝牙漫游认证器,以及指纹和3D面容的平台认证器,进行安全认证。

您将建立什么

在这个Codelab中,您将使用已经创建好的Demo Project实现对华为FIDO2客户端API调用,通过DemoProject你可以体验到:

您将会学到什么

其他特性

本地生物特征认证

硬件要求

软件要求

集成HUAWEI HMS Core能力,需要完成以下准备工作:

具体操作,请按照《HUAWEI HMS Core集成准备》中详细说明来完成。

打开服务开关

  1. 在项目的应用信息页面,选择API管理页签。
  2. 打开"FIDO"服务开关。

添加当前应用的AGC配置文件

将下载的"agconnect-services.json"文件移至Android Studio开发工程app的根目录下。

配置HMS Core SDK的Maven仓地址

打开Android Studio项目级build.gradle文件

在allprojects > repositories里面配置HMS Core SDK的Maven仓地址

allprojects { repositories { google() jcenter() // Add the following line maven { url 'https://developer.huawei.com/repo/' } } }

在buildscript > repositories里面配置HMS Core SDK的Maven仓地址

buildscript { repositories { google() jcenter() // Add the following line maven { url 'https://developer.huawei.com/repo/' } } }

在buildscript > dependencies里面增加配置

buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' // Add the following line classpath 'com.huawei.agconnect:agcp:1.3.1.300' } }

添加编译依赖

打开应用级的build.gradle文件

在"dependencies"中添加编译依赖

dependencies { implementation 'com.huawei.hms:fido-fido2:5.0.2.303' }

在文件头添加配置

apply plugin: 'com.huawei.agconnect'

在"android"中配置签名

将从能力接入准备中获取的JKS文件(如FIDO BioAuthn Android Sample.jks)拷贝到工程的app目录下。同时在build.gradle文件中配置签名。

android { signingConfigs { config { keyAlias 'FIDO BioAuthn Android Sample' keyPassword '******' storeFile file('FIDO BioAuthn Android Sample.jks') storePassword '******' } } buildTypes { debug { signingConfig signingConfigs.config } release { signingConfig signingConfigs.config minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } }

点击Sync Now同步工程

FIDO2分成两个操作,注册和认证,处理流程类似。过程如下:

  1. 创建一个Activity。
  2. 从FIDO服务器获取挑战值及相关策略,并组装请求消息。(这里仅提供FIDO客户端的API,涉及与FIDO服务器的交互,请参考相关规范,并联系FIDO服务器供应商获取相关接口说明)
  3. 调用Fido2Client.getRegistrationIntent()发起注册。或调用Fido2Client.getAuthenticationIntent()发起认证。
  4. 上一步调用成功后,在回调中调用Fido2Intent.launchFido2Activity(),启动注册(requestCode为Fido2Client.REGISTRATION_REQUEST)或认证(requestCode为Fido2Client.AUTHENTICATION_REQUEST)。该回调在主线程中执行。
  5. 在回调Activity.onActivityResult()中,调用Fido2Client.getFido2RegistrationResponse()或Fido2Client.getFido2AuthenticationResponse()获取注册或认证的结果。
  6. 把注册或认证的结果发送给FIDO服务器进行验证。(这里仅提供FIDO客户端的API,涉及与FIDO服务器的交互,请参考相关规范,并联系FIDO服务器供应商获取相关接口说明)

FIDO2 相关API使用方法,如以下代码片段所示:

Java语言:

package com.huawei.hms.kit.fidocp; import android.app.Activity; import android.app.AlertDialog; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import com.huawei.hms.fidocp.R; import com.huawei.hms.support.api.fido.fido2.Algorithm; import com.huawei.hms.support.api.fido.fido2.Attachment; import com.huawei.hms.support.api.fido.fido2.AttestationConveyancePreference; import com.huawei.hms.support.api.fido.fido2.AuthenticatorMetadata; import com.huawei.hms.support.api.fido.fido2.AuthenticatorSelectionCriteria; import com.huawei.hms.support.api.fido.fido2.Fido2; import com.huawei.hms.support.api.fido.fido2.Fido2AuthenticationRequest; import com.huawei.hms.support.api.fido.fido2.Fido2AuthenticationResponse; import com.huawei.hms.support.api.fido.fido2.Fido2Client; import com.huawei.hms.support.api.fido.fido2.Fido2Extension; import com.huawei.hms.support.api.fido.fido2.Fido2Intent; import com.huawei.hms.support.api.fido.fido2.Fido2IntentCallback; import com.huawei.hms.support.api.fido.fido2.Fido2RegistrationRequest; import com.huawei.hms.support.api.fido.fido2.Fido2RegistrationResponse; import com.huawei.hms.support.api.fido.fido2.Fido2Response; import com.huawei.hms.support.api.fido.fido2.NativeFido2AuthenticationOptions; import com.huawei.hms.support.api.fido.fido2.NativeFido2RegistrationOptions; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialCreationOptions; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialDescriptor; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialParameters; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialRequestOptions; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialRpEntity; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialType; import com.huawei.hms.support.api.fido.fido2.PublicKeyCredentialUserEntity; import java.io.UnsupportedEncodingException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; public class Fido2DemoActivity extends AppCompatActivity { private TextView reusltView; private Fido2Client fido2Client; private byte[] regCredentialId = null; // 根据实际业务指定 private String rpId = "com.huawei.hms.fido2.test"; // 根据实际业务指定 private String user = "fidoCp"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fido2_demo); reusltView = findViewById(R.id.reusltView); fido2Client = Fido2.getFido2Client(this); } // 从FIDO服务获取挑战值及相关策略,并组装Fido2RegistrationRequest请求消息。 private Fido2RegistrationRequest assembleFido2RegistrationRequest() { // TODO 从FIDO服务获取挑战值及相关策略。 byte[] challengeBytes = getChallege(); // 组装Fido2RegistrationRequest请求消息。AuthenticatorSelectionCriteria构造函数的第一个参数 // 为指定使用平台认证器还是漫游认证器,如果没有指定需求,可以不指定,传null。 PublicKeyCredentialCreationOptions.Builder builder = new PublicKeyCredentialCreationOptions.Builder(); builder.setRp(new PublicKeyCredentialRpEntity(rpId, rpId, null)) .setChallenge(challengeBytes) .setAttestation(AttestationConveyancePreference.DIRECT) .setAuthenticatorSelection(new AuthenticatorSelectionCriteria(Attachment.PLATFORM, null, null)) .setPubKeyCredParams(new ArrayList<PublicKeyCredentialParameters>( Arrays.asList( new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.ES256), new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.RS256)))) .setTimeoutSeconds(60L); try { builder.setUser(new PublicKeyCredentialUserEntity(user, user.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { reusltView.append(e.getMessage() + '\n'); } if (regCredentialId != null) { builder.setExcludeList(new ArrayList<PublicKeyCredentialDescriptor>( Arrays.asList( new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, regCredentialId)))); } HashMap<String, Object> extensions = new HashMap<>(); builder.setExtensions(extensions); // 指定特定的平台认证器,及相关扩展项。如果没有需求,可以不指定。 useSelectedPlatformAuthenticator(extensions); return new Fido2RegistrationRequest(builder.build(), null); } public void btnRegistrationClicked(View view) { if (!fido2Client.isSupported()) { showMsg("不支持FIDO2。"); return; } Fido2RegistrationRequest request = assembleFido2RegistrationRequest(); // 调用Fido2Client.getRegistrationIntent获取一个Fido2Intent实例,并启动FIDO客户端注册流程。 fido2Client.getRegistrationIntent(request, NativeFido2RegistrationOptions.DEFAULT_OPTIONS, new Fido2IntentCallback() { @Override public void onSuccess(Fido2Intent fido2Intent) { // 通过Fido2Client.REGISTRATION_REQUEST,启动FIDO客户端注册流程。 fido2Intent.launchFido2Activity(Fido2DemoActivity.this, Fido2Client.REGISTRATION_REQUEST); } @Override public void onFailure(int errorCode, CharSequence errString) { showError("注册失败。" + errorCode + "=" + errString); } }); } // 从FIDO服务获取挑战值及相关策略,并组装Fido2AuthenticationRequest请求消息。 private Fido2AuthenticationRequest assembleFido2AuthenticationRequest() { // TODO 从FIDO服务获取挑战值及相关策略。 byte[] challengeBytes = getChallege(); // 组装Fido2RegistrationRequest请求消息。 List<PublicKeyCredentialDescriptor> allowList = new ArrayList<>(); allowList.add(new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, regCredentialId)); PublicKeyCredentialRequestOptions.Builder builder = new PublicKeyCredentialRequestOptions.Builder(); builder.setRpId(rpId).setChallenge(challengeBytes).setAllowList(allowList).setTimeoutSeconds(60L); HashMap<String, Object> extensions = new HashMap<>(); builder.setExtensions(extensions); // 指定特定的平台认证器,及相关扩展项。如果没有需求,可以不指定。 useSelectedPlatformAuthenticator(extensions); return new Fido2AuthenticationRequest(builder.build(), null); } public void btnAuthenticationClicked(View view) { if (regCredentialId == null) { showMsg("请先进行注册。"); return; } if (!fido2Client.isSupported()) { showMsg("不支持FIDO2。"); return; } Fido2AuthenticationRequest request = assembleFido2AuthenticationRequest(); // 调用Fido2Client.getAuthenticationIntent获取一个Fido2Intent实例,并启动FIDO客户端认证流程。 fido2Client.getAuthenticationIntent(request, NativeFido2AuthenticationOptions.DEFAULT_OPTIONS, new Fido2IntentCallback() { @Override public void onSuccess(Fido2Intent fido2Intent) { // 通过Fido2Client.AUTHENTICATION_REQUEST,启动FIDO客户端注册流程。 fido2Intent.launchFido2Activity(Fido2DemoActivity.this, Fido2Client.AUTHENTICATION_REQUEST); } @Override public void onFailure(int errorCode, CharSequence errString) { showError("认证失败。" + errorCode + "=" + errString); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != Activity.RESULT_OK) { showMsg("未知错误。"); return; } switch (requestCode) { // Receive registration response case Fido2Client.REGISTRATION_REQUEST: Fido2RegistrationResponse fido2RegistrationResponse = fido2Client.getFido2RegistrationResponse(data); if (fido2RegistrationResponse.isSuccess()) { reusltView.append("注册\n"); reusltView.append(fido2RegistrationResponse.getAuthenticatorAttestationResponse().toJson()); reusltView.append("\n"); // save the credentialId regCredentialId = fido2RegistrationResponse.getAuthenticatorAttestationResponse().getCredentialId(); showMsg("注册成功。"); } else { showError("注册失败。", fido2RegistrationResponse); } break; // Receive authentication response case Fido2Client.AUTHENTICATION_REQUEST: Fido2AuthenticationResponse fido2AuthenticationResponse = fido2Client.getFido2AuthenticationResponse(data); if (fido2AuthenticationResponse.isSuccess()) { reusltView.append("认证\n"); reusltView.append(fido2AuthenticationResponse.getAuthenticatorAssertionResponse().toJson()); reusltView.append("\n"); showMsg("认证成功。"); } else { showError("认证失败。", fido2AuthenticationResponse); } } } private void showError(String message) { AlertDialog.Builder builder = new AlertDialog.Builder(Fido2DemoActivity.this); builder.setTitle("错误"); builder.setMessage(message); builder.setPositiveButton("确认", null); builder.show(); } private void showError(String message, Fido2Response fido2Response) { StringBuilder errMsgBuilder = new StringBuilder(); errMsgBuilder.append(message) .append(fido2Response.getFido2Status()) .append("=") .append(fido2Response.getFido2StatusMessage()) .append(String.format(Locale.getDefault(), "(Ctap错误:0x%x=%s)", fido2Response.getCtapStatus(), fido2Response.getCtapStatusMessage())); showError(errMsgBuilder.toString()); } private void showMsg(String message) { AlertDialog.Builder builder = new AlertDialog.Builder(Fido2DemoActivity.this); builder.setTitle("提示"); builder.setMessage(message); builder.setPositiveButton("确认", null); builder.show(); } public void btnClearLogClicked(View view) { reusltView.setText(""); regCredentialId = null; } private byte[] getChallege() { return SecureRandom.getSeed(16); } // 选择使用特定的平台认证器,并设定相关扩展项 private void useSelectedPlatformAuthenticator(HashMap<String, Object> extensions) { if (!fido2Client.hasPlatformAuthenticators()) { return; } List<String> selectedAuthenticatorList = new ArrayList<>(); for (AuthenticatorMetadata meta : fido2Client.getPlatformAuthenticators()) { // 指纹认证器 if (meta.isSupportedUvm(AuthenticatorMetadata.UVM_FINGERPRINT)) { selectedAuthenticatorList.add(meta.getAaguid()); if (meta.getExtensions().contains(Fido2Extension.W3C_WEBAUTHN_UVI.getIdentifier())) { // 是否验证指纹ID,为true,注册和验证,必须使用相同的手指。 extensions.put(Fido2Extension.W3C_WEBAUTHN_UVI.getIdentifier(), Boolean.TRUE); } if (meta.getExtensions().contains(Fido2Extension.HMS_R_PA_CIBBE_01.getIdentifier())) { // 认证凭据在生物特征变化时是否失效,为true或者不设置该扩展参数,录入指纹时密钥会失效,仅注册有效。 extensions.put(Fido2Extension.HMS_R_PA_CIBBE_01.getIdentifier(), Boolean.TRUE); } } // 面容锁屏认证器 else if (meta.isSupportedUvm(AuthenticatorMetadata.UVM_FACEPRINT)) { // selectedAuthenticatorList.add(authenticatorMetadata.getAaguid()); } } extensions.put(Fido2Extension.HMS_RA_C_PACL_01.getIdentifier(), selectedAuthenticatorList); } }

Kotlin语言:

package com.huawei.hms.kit.fidocp import android.app.Activity import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.view.View import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.huawei.hms.fidocp.R import com.huawei.hms.support.api.fido.fido2.* import java.io.UnsupportedEncodingException import java.security.SecureRandom import java.util.* class Fido2DemoActivity : AppCompatActivity() { private var reusltView: TextView? = null private var fido2Client: Fido2Client? = null private var regCredentialId: ByteArray? = null // 根据实际业务指定 private val rpId = "com.huawei.hms.fido2.test" // 根据实际业务指定 private val user = "fidoCp" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_fido2_demo) reusltView = findViewById(R.id.reusltView) fido2Client = Fido2.getFido2Client(this) } // 从FIDO服务获取挑战值及相关策略,并组装Fido2RegistrationRequest请求消息。 private fun assembleFido2RegistrationRequest(): Fido2RegistrationRequest { // TODO 从FIDO服务获取挑战值及相关策略。 val challengeBytes = challege // 组装Fido2RegistrationRequest请求消息。AuthenticatorSelectionCriteria构造函数的第一个参数 // 为指定使用平台认证器还是漫游认证器,如果没有指定需求,可以不指定,传null。 val builder = PublicKeyCredentialCreationOptions.Builder() builder.setRp(PublicKeyCredentialRpEntity(rpId, rpId, null)) .setUser(PublicKeyCredentialUserEntity(user, user.toByteArray())) .setChallenge(challengeBytes) .setAttestation(AttestationConveyancePreference.DIRECT) .setAuthenticatorSelection(AuthenticatorSelectionCriteria(Attachment.PLATFORM, null, null)) .setPubKeyCredParams(ArrayList( Arrays.asList(PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.ES256), PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.RS256)))) .setTimeoutSeconds(60L) try { builder.setUser(PublicKeyCredentialUserEntity(user, user.toByteArray(charset("UTF-8")))) } catch (e: UnsupportedEncodingException) { reusltView!!.append(e.message + "\n") } if (regCredentialId != null) { builder.setExcludeList(ArrayList( Arrays.asList(PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, regCredentialId)))) } val extensions = HashMap<String, Any>() builder.setExtensions(extensions) // 指定特定的平台认证器,及相关扩展项。如果没有需求,可以不指定。 useSelectedPlatformAuthenticator(extensions) return Fido2RegistrationRequest(builder.build(), null) } fun btnRegistrationClicked(view: View?) { if (!fido2Client!!.isSupported) { showMsg("不支持FIDO2。") return } val request = assembleFido2RegistrationRequest() // 调用Fido2Client.getRegistrationIntent获取一个Fido2Intent实例,并启动FIDO客户端注册流程。 fido2Client!!.getRegistrationIntent(request, NativeFido2RegistrationOptions.DEFAULT_OPTIONS, object : Fido2IntentCallback { override fun onSuccess(fido2Intent: Fido2Intent) { // 通过Fido2Client.REGISTRATION_REQUEST,启动FIDO客户端注册流程。 fido2Intent.launchFido2Activity(this@Fido2DemoActivity, Fido2Client.REGISTRATION_REQUEST) } override fun onFailure(errorCode: Int, errString: CharSequence) { showError("注册失败。$errorCode=$errString") } }) } // 从FIDO服务获取挑战值及相关策略,并组装Fido2AuthenticationRequest请求消息。 private fun assembleFido2AuthenticationRequest(): Fido2AuthenticationRequest { // TODO 从FIDO服务获取挑战值及相关策略。 val challengeBytes = challege // 组装Fido2RegistrationRequest请求消息。 val allowList: MutableList<PublicKeyCredentialDescriptor> = ArrayList() allowList.add(PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, regCredentialId)) val builder = PublicKeyCredentialRequestOptions.Builder() builder.setRpId(rpId).setChallenge(challengeBytes).setAllowList(allowList).setTimeoutSeconds(60L) val extensions = HashMap<String, Any>() builder.setExtensions(extensions) // 指定特定的平台认证器,及相关扩展项。如果没有需求,可以不指定。 useSelectedPlatformAuthenticator(extensions) return Fido2AuthenticationRequest(builder.build(), null) } fun btnAuthenticationClicked(view: View?) { if (regCredentialId == null) { showMsg("请先进行注册。") return } if (!fido2Client!!.isSupported) { showMsg("不支持FIDO2。") return } val request = assembleFido2AuthenticationRequest() // 调用Fido2Client.getAuthenticationIntent获取一个Fido2Intent实例,并启动FIDO客户端认证流程。 fido2Client!!.getAuthenticationIntent(request, NativeFido2AuthenticationOptions.DEFAULT_OPTIONS, object : Fido2IntentCallback { override fun onSuccess(fido2Intent: Fido2Intent) { // 通过Fido2Client.AUTHENTICATION_REQUEST,启动FIDO客户端注册流程。 fido2Intent.launchFido2Activity(this@Fido2DemoActivity, Fido2Client.AUTHENTICATION_REQUEST) } override fun onFailure(errorCode: Int, errString: CharSequence) { showError("认证失败。$errorCode=$errString") } }) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode != Activity.RESULT_OK) { showMsg("未知错误。") return } when (requestCode) { Fido2Client.REGISTRATION_REQUEST -> { val fido2RegistrationResponse = fido2Client!!.getFido2RegistrationResponse(data) if (fido2RegistrationResponse.isSuccess) { reusltView!!.append("注册\n") reusltView!!.append(fido2RegistrationResponse.authenticatorAttestationResponse.toJson()) reusltView!!.append("\n") // save the credentialId regCredentialId = fido2RegistrationResponse.authenticatorAttestationResponse.credentialId showMsg("注册成功。") } else { showError("注册失败。", fido2RegistrationResponse) } } Fido2Client.AUTHENTICATION_REQUEST -> { val fido2AuthenticationResponse = fido2Client!!.getFido2AuthenticationResponse(data) if (fido2AuthenticationResponse.isSuccess) { reusltView!!.append("认证\n") reusltView!!.append(fido2AuthenticationResponse.authenticatorAssertionResponse.toJson()) reusltView!!.append("\n") showMsg("认证成功。") } else { showError("认证失败。", fido2AuthenticationResponse) } } } } private fun showError(message: String) { val builder = AlertDialog.Builder(this@Fido2DemoActivity) builder.setTitle("错误") builder.setMessage(message) builder.setPositiveButton("确认", null) builder.show() } private fun showError(message: String, fido2Response: Fido2Response) { val errMsgBuilder = StringBuilder() errMsgBuilder.append(message) .append(fido2Response.fido2Status) .append("=") .append(fido2Response.fido2StatusMessage) .append(String.format(Locale.getDefault(), "(Ctap错误:0x%x=%s)", fido2Response.ctapStatus, fido2Response.ctapStatusMessage)) showError(errMsgBuilder.toString()) } private fun showMsg(message: String) { val builder = AlertDialog.Builder(this@Fido2DemoActivity) builder.setTitle("提示") builder.setMessage(message) builder.setPositiveButton("确认", null) builder.show() } fun btnClearLogClicked(view: View?) { reusltView!!.text = "" regCredentialId = null } private val challege: ByteArray get() = SecureRandom.getSeed(16) // 选择使用特定的平台认证器,并设定相关扩展项 private fun useSelectedPlatformAuthenticator(extensions: HashMap<String, Any>) { if (!fido2Client!!.hasPlatformAuthenticators()) { return } val selectedAuthenticatorList: MutableList<String> = ArrayList() for (meta in fido2Client!!.platformAuthenticators) { if (!meta.isAvailable) { continue } // 指纹认证器 if (meta.isSupportedUvm(AuthenticatorMetadata.UVM_FINGERPRINT)) { selectedAuthenticatorList.add(meta.aaguid) if (meta.extensions.contains(Fido2Extension.W3C_WEBAUTHN_UVI.identifier)) { // 是否验证指纹ID,为true,注册和验证,必须使用相同的手指。 extensions[Fido2Extension.W3C_WEBAUTHN_UVI.identifier] = true } if (meta.extensions.contains(Fido2Extension.HMS_R_PA_CIBBE_01.identifier)) { // 认证凭据在生物特征变化时是否失效,为true或者不设置该扩展参数,录入指纹时密钥会失效,仅注册有效。 extensions[Fido2Extension.HMS_R_PA_CIBBE_01.identifier] = true } } else if (meta.isSupportedUvm(AuthenticatorMetadata.UVM_FACEPRINT)) { // selectedAuthenticatorList.add(authenticatorMetadata.getAaguid()); } } extensions[Fido2Extension.HMS_RA_C_PACL_01.identifier] = selectedAuthenticatorList } }

安装测试APK,点击注册或认证按钮。

干得好,您已经成功完成了Codelab并学到了:

您可以阅读下面链接,了解更多相关的信息。

相关文档

您可以点击下方按钮下载源码。

源码下载

已复制代码