Xamarin.Form框架并没有提供指纹认证功能,需要分平台实现!
Android的Fingerprint Authentication
参考:https://docs.microsoft.com/zh-cn/xamarin/android/platform/fingerprint-authentication/
概述
指纹扫描仪在Android设备上的到来为应用程序提供了用户身份验证的传统用户名/密码方法的替代方案。
使用指纹对用户进行身份验证可以使应用程序合并比用户名和密码更不安全的安全性。
FingerprintManager API使用指纹扫描仪来定位设备,并且运行的API级别为23(Android 6.0)或更高。 这些API在Android.Hardware.Fingerprints命名空间中找到。 Android支持库v4提供了适用于旧版Android的指纹API版本。 兼容性API在Android.Support.v4.Hardware.Fingerprint命名空间中找到,通过Xamarin.Android.Support.v4 NuGet包进行分发。
FingerprintManager(及其支持库对应的FingerprintManagerCompat)是使用指纹扫描硬件的主要类。 此类是围绕系统级服务的Android SDK包装,该服务管理与硬件本身的交互。 它负责启动指纹扫描仪并响应来自扫描仪的反馈。 此类具有一个非常简单的接口,只有三个成员:
- Authenticate 身份验证–此方法将初始化硬件扫描程序并在后台启动服务,等待用户扫描其指纹。
- EnrolledFingerprints –如果用户已在设备上注册一个或多个指纹,则此属性将返回true。
- HardwareDetected –此属性用于确定设备是否支持指纹扫描。
以下代码段是如何使用支持库兼容性API调用它的示例:
// context is any Android.Content.Context instance, typically the Activity FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.From(context); fingerprintManager.Authenticate(FingerprintManager.CryptoObject crypto,int flags,CancellationSignal cancel, FingerprintManagerCompat.AuthenticationCallback callback,Handler handler );
第一个参数是用于通过指纹验证取出AndroidKeyStore中的key的对象,稍后描述。
第二个参数可以用来取消指纹验证,如果想手动关闭验证,可以调用该参数的cancel方法。
第三个参数没什么意义,就是传0就好了。
第四个参数最重要,由于指纹信息是存在系统硬件中的,app是不可以访问指纹信息的,所以每次验证的时候,系统会通过这个callback告诉你是否验证通过、验证失败等。
第五个参数是handler,fingerprint中的消息都通过这个handler来传递消息,如果你传空,则默认创建一个在主线程上的handler来传递消息,没什么用,传null好了。
————————————————
本指南将讨论如何使用FingerprintManager API通过指纹认证来增强Android应用程序。
它将介绍如何实例化和创建CryptoObject来帮助保护指纹扫描仪的结果。 我们将研究应用程序应如何子类化FingerprintManager.AuthenticationCallback并响应指纹扫描仪的反馈。 最后,我们将看到如何在Android设备或仿真器上注册指纹,以及如何使用adb模拟指纹扫描。
CryptoObjec类封装了基于javax.crypto.Cipher的CryptoObject的创建
要求
指纹认证需要Android 6.0(API级别23)或更高版本以及具有指纹扫描器的设备。
必须为要认证的每个用户在设备上注册一个指纹。
这涉及设置使用密码,PIN,滑动模式或面部识别的屏幕锁定。 可以在Android仿真器中模拟某些指纹认证功能。 有关这两个主题的更多信息,请参见“注册指纹”部分。
【即 要调用指纹认证的应用,此手机必须设置了一个锁屏密码 和 录制了至少一个指纹】
入门
首先,让我们先介绍如何配置 Xamarin Android 项目,使应用程序能够使用指纹身份验证:
- 更新androidmanifest.xml以声明指纹 api 所需的权限。
- 获取对
FingerprintManager
的引用。 - 检查设备是否能够进行指纹扫描。
1、Android 应用程序必须在清单中请求 USE_FINGERPRINT
权限。
2、 获取FingerprintManager的实例
接下来,应用程序必须获取FingerprintManager或FingerprintManagerCompat类的实例。 为了与旧版本的Android兼容,Android应用程序应使用Android支持v4 NuGet包中提供的兼容性API。 以下代码段演示了如何从操作系统中获取适当的对象:
// Using the Android Support Library v4 FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.From(context); // Using API level 23: FingerprintManager fingerprintManager = context.GetSystemService(Context.FingerprintService) as FingerprintManager;
context是任何Android的 Android.Content.Context。 通常,这是执行身份验证的Activity 。
3、检查资格
应用程序必须执行多项检查,以确保可以使用指纹身份验证。 总共,应用程序使用五个条件来检查资格:
- API级别23 –指纹API要求API级别23或更高。 FingerprintManagerCompat类将为您包装API级别检查。 因此,建议使用Android Support Library v4 和FingerprintManagerCompat。
- 硬件–首次启动应用程序时,应检查设备是否存在指纹扫描仪:
FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.From(context); if (!fingerprintManager.IsHardwareDetected) { // Code omitted }
- 设备安全–用户必须使用屏幕锁保护设备。 如果用户尚未使用屏幕锁保护设备,并且安全性对于应用程序很重要,则应通知用户必须配置屏幕锁。 以下代码段显示了如何检查此先决条件:
KeyguardManager keyguardManager = (KeyguardManager) GetSystemService(KeyguardService); if (!keyguardManager.IsKeyguardSecure) { }
- 登记(注册)的指纹–用户必须至少在操作系统上注册了一个指纹。 此权限检查应在每次身份验证尝试之前进行:
FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.From(context); if (!fingerprintManager.HasEnrolledFingerprints) { // Can‘t use fingerprint authentication - notify the user that they need to // enroll at least one fingerprint with the device. }
- 权限–使用该应用程序之前,该应用程序必须向用户请求权限。 对于Android 5.0及更低版本,用户将授予权限作为安装应用程序的条件。 Android 6.0引入了新的权限模型,该模型可在运行时检查权限。 此代码段是如何在Android 6.0上检查权限的示例:【应用程序必须授予使用指纹扫描仪的权限。在项目属性的清单中添加】
// The context is typically a reference to the current activity. Android.Content.PM.Permission permissionResult = ContextCompat.CheckSelfPermission(context, Manifest.Permission.UseFingerprint); if (permissionResult == Android.Content.PM.Permission.Granted) { // Permission granted - go ahead and start the fingerprint scanner. } else { // No permission. Go and ask for permissions and don‘t start the scanner. See // https://developer.android.com/training/permissions/requesting.html }
每次应用程序提供身份验证选项时,都要检查所有这些条件,以确保用户获得最佳的用户体验。 其设备或操作系统的更改或升级可能会影响指纹身份验证的可用性。 如果您选择缓存任何这些检查的结果,请确保满足升级方案。
有关如何在Android 6.0中请求权限的更多信息,请参阅Android指南“在运行时请求权限”。
扫描指纹
指纹身份验证工作流的快速概述:【监听,回调,自己写UI】
- 调用
FingerprintManager.Authenticate
,同时传递CryptoObject
和FingerprintManager.AuthenticationCallback
的实例。CryptoObject
用于确保指纹身份验证结果未被篡改。 - 将FingerprintManager. authenticationcallback 传递给类的子类。 指纹身份验证开始时,将提供此类的实例以
FingerprintManager
。 指纹扫描器完成后,它将调用此类的一个回调方法。 - 编写代码以更新 UI,以让用户知道设备已启动指纹扫描器并等待用户交互。
- 指纹扫描器完成后,Android 会通过对上一步中提供的
FingerprintManager.AuthenticationCallback
实例调用方法,将结果返回到应用程序。 - 应用程序将向用户通知指纹身份验证结果,并根据需要对结果做出反应。
取消指纹扫描
用户(或应用程序)在启动指纹扫描后可能需要取消指纹扫描。 在这种情况下,请在提供给FingerprintManager的CancellationSignal上调用IsCancelled方法,并在调用它以启动指纹扫描时进行身份验证。
现在我们已经看到了Authenticate方法,让我们更详细地研究一些更重要的参数。
首先,我们将研究响应身份验证回调,它将讨论如何对FingerprintManager.AuthenticationCallback进行子类化,从而使Android应用程序能够对指纹扫描仪提供的结果做出反应。
Creating a CryptoObject
指纹认证结果的完整性对应用程序很重要-这就是应用程序如何知道用户身份的方式。
从理论上讲,第三方恶意软件可能会拦截和篡改指纹扫描仪返回的结果。 本节将讨论一种保留指纹结果有效性的技术。
FingerprintManager.CryptoObject是Java加密API的包装,FingerprintManager使用它来保护身份验证请求的完整性。
通常,Javax.Crypto.Cipher对象是用于加密指纹扫描器结果的机制。 Cipher对象【意思:密码】本身将使用由应用程序使用Android密钥库API创建的密钥。
为了了解这些类如何协同工作,让我们首先看下面的代码,该代码演示如何创建CryptoObject,然后更详细地进行解释:
public class CryptoObjectHelper { // This can be key name you want. Should be unique for the app. static readonly string KEY_NAME = "com.xamarin.android.sample.fingerprint_authentication_key"; // We always use this keystore on Android. static readonly string KEYSTORE_NAME = "AndroidKeyStore"; // Should be no need to change these values. static readonly string KEY_ALGORITHM = KeyProperties.KeyAlgorithmAes; static readonly string BLOCK_MODE = KeyProperties.BlockModeCbc; static readonly string ENCRYPTION_PADDING = KeyProperties.EncryptionPaddingPkcs7; static readonly string TRANSFORMATION = KEY_ALGORITHM + "/" + BLOCK_MODE + "/" + ENCRYPTION_PADDING; readonly KeyStore _keystore; public CryptoObjectHelper() { _keystore = KeyStore.GetInstance(KEYSTORE_NAME); _keystore.Load(null); } public FingerprintManagerCompat.CryptoObject BuildCryptoObject() { Cipher cipher = CreateCipher(); return new FingerprintManagerCompat.CryptoObject(cipher); } Cipher CreateCipher(bool retry = true) { IKey key = GetKey(); Cipher cipher = Cipher.GetInstance(TRANSFORMATION); try { cipher.Init(CipherMode.EncryptMode, key); } catch(KeyPermanentlyInvalidatedException e) { _keystore.DeleteEntry(KEY_NAME); if(retry) { CreateCipher(false); } else { throw new Exception("Could not create the cipher for fingerprint authentication.", e); } } return cipher; } IKey GetKey() { IKey secretKey; if(!_keystore.IsKeyEntry(KEY_NAME)) { CreateKey(); } secretKey = _keystore.GetKey(KEY_NAME, null); return secretKey; } void CreateKey() { KeyGenerator keyGen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, KEYSTORE_NAME); KeyGenParameterSpec keyGenSpec = new KeyGenParameterSpec.Builder(KEY_NAME, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt) .SetBlockModes(BLOCK_MODE) .SetEncryptionPaddings(ENCRYPTION_PADDING) .SetUserAuthenticationRequired(true) .Build(); keyGen.Init(keyGenSpec); keyGen.GenerateKey(); } }
程序将使用应用程序创建的key为每个CryptoObject创建一个新的Cipher。key由在CryptoObjectHelper类的开头设置的KEY_NAME变量标识。
- 获取密钥IKey:方法GetKey将尝试使用Android Keystore API检索密钥。如果密钥不存在,则方法CreateKey将为应用程序创建一个新密钥。
- 实例化Cipher:通过调用Cipher.GetInstance实例化cipher,并进行转换(参数:一个字符串值,该值告诉密码如何加密和解密数据)。
- 初始化Cipher:对Cipher.Init的调用 将通过提供来自应用程序的密钥来完成cipher的初始化。
请务必意识到,在某些情况下Android可能会使密钥无效:
- 设备已注册了新的指纹。
- 设备没有登记指纹。
- 用户已禁用屏幕锁定。
- 用户已更改屏幕锁定(屏幕锁定的类型或所使用的PIN /图案)。
发生这种情况时,Cipher.Init将抛出KeyPermanentlyInvalidatedException。上面的示例代码将捕获该异常,删除key,然后创建一个新。
下一节将讨论如何创建密钥并将其存储在设备上。
创建密钥key
CryptoObjectHelper类使用Android KeyGenerator来创建密钥并将其存储在设备上。 KeyGenerator类可以创建密钥,但是需要一些有关要创建的密钥类型的元数据。此信息由KeyGenParameterSpec类的实例提供。
使用GetInstance工厂方法实例化KeyGenerator。该示例代码使用 高级加密标准(AES)作为加密算法。 AES会将数据分成固定大小的块,并对每个块进行加密。
接下来,使用KeyGenParameterSpec.Builder创建一个KeyGenParameterSpec。 KeyGenParameterSpec.Builder包装了有关要创建的密钥的以下信息:
- 密钥名称。
- 该密钥对于加密和解密都必须有效。
- 在示例代码中,BLOCK_MODE设置为密码块链接(KeyProperties.BlockModeCbc),这意味着每个块都与前一个块进行XOR(在每个块之间创建依赖项)。
- CryptoObjectHelper使用公共密钥密码标准7(PKCS7)生成字节,这些字节将填充这些块以确保它们的大小相同。
- SetUserAuthenticationRequired(true)表示必须先进行用户认证,然后才能使用密钥。
一旦创建了KeyGenParameterSpec,它将用于初始化KeyGenerator,它将生成密钥并将其安全地存储在设备上。
【总结:Android.KeyGenerator创建密钥并将其存储在设备上,然后用Javax.Crypto.Cipher 拿创建的密钥 加密指纹扫描器结果,防止扫描结果被篡改】
Using the CryptoObjectHelper
现在,示例代码已将创建CryptoWrapper的大部分逻辑封装到CryptoObjectHelper类中,让我们从本指南的开头重新访问代码,并使用CryptoObjectHelper创建密码并启动指纹扫描器:
protected void FingerPrintAuthenticationExample() { const int flags = 0; /* always zero (0) */ CryptoObjectHelper cryptoHelper = new CryptoObjectHelper(); cancellationSignal = new Android.Support.V4.OS.CancellationSignal(); // Using the Support Library classes for maximum reach FingerprintManagerCompat fingerPrintManager = FingerprintManagerCompat.From(this); // AuthCallbacks is a C# class defined elsewhere in code. FingerprintManagerCompat.AuthenticationCallback authenticationCallback = new MyAuthCallbackSample(this); // Here is where the CryptoObjectHelper builds the CryptoObject. fingerprintManager.Authenticate(cryptohelper.BuildCryptoObject(), flags, cancellationSignal, authenticationCallback, null); }
Responding to Authentication Callbacks
指纹扫描仪在其自己的后台线程上运行,完成后它将通过在UI线程上调用FingerprintManager.AuthenticationCallback的一种方法来报告扫描结果。 Android应用程序必须提供自己的处理程序,该处理程序扩展此抽象类,并实现以下所有方法:
- OnAuthenticationError(int errorCode,ICharSequence errString)–发生不可恢复的错误时调用。 除了可以再试一次以外,应用程序或用户无法采取其他措施来纠正这种情况。
- OnAuthenticationFailed()–当检测到指纹但设备无法识别指纹时,将调用此方法。
- OnAuthenticationHelp(int helpMsgId,ICharSequence helpString)–在发生可恢复的错误(例如手指在扫描仪上快速滑动)时调用。
- OnAuthenticationSucceeded(FingerprintManagerCompati.AuthenticationResult结果)–识别指纹后调用此方法。
如果在调用Authenticate时使用了CryptoObject,建议在OnAuthenticationSuccessful中调用Cipher.DoFinal。 如果密码被篡改或初始化不正确,DoFinal将抛出异常,表明指纹扫描仪的结果可能已在应用程序外部被篡改。
具体说明:
1、OnAuthenticationSucceeded检查调用身份验证时是否向密码管理器提供了密码。如果是这样,则在密码上调用DoFinal方法。这将关闭密码,将其还原到其原始状态。如果密码有问题,则DoFinal将引发异常,并且身份验证尝试应被视为失败。
2、OnAuthenticationError和OnAuthenticationHelp回调每个都接收一个整数,指示问题出在哪里。以下部分说明了每种可能的帮助或错误代码。这两个回调起到类似的作用-通知应用程序指纹认证失败。它们的不同之处在于严重性。 OnAuthenticationHelp是用户可恢复的错误,例如快速滑动指纹。 OnAuthenticationError是更严重的错误,例如损坏的指纹扫描仪。
请注意,当通过CancellationSignal.Cancel()消息取消指纹扫描时,将调用OnAuthenticationError。 errMsgId参数的值为5(FingerprintState.ErrorCanceled)。根据要求,AuthenticationCallbacks的实现可能会与其他错误区别对待。
3、成功扫描指纹但与设备注册的任何指纹都不匹配时,将调用OnAuthenticationFailed。
指纹身份验证指南/建议
现在,我们已经了解了围绕Android 6.0指纹认证的概念和API,下面我们讨论一些有关使用指纹API的一般建议。
1、Use the Android Support Library v4 Compatibility APIs –通过从代码中删除API检查,从而简化应用程序代码,并使应用程序可以定位到尽可能多的设备。
2、提供指纹认证的替代方法–指纹认证是应用程序认证用户的一种简便快捷的方法,但是,不能认为它会一直有效或可用。指纹扫描仪可能会出现故障,镜头可能变脏,用户可能未将设备配置为使用指纹身份验证,或者此后指纹丢失了。用户也可能不希望在您的应用程序中使用指纹认证。由于这些原因,Android应用程序应提供备用的身份验证过程,例如用户名和密码。
3、使用Google的指纹图标–所有应用程序都应使用Google提供的相同指纹图标。使用标准图标可以使Android用户轻松识别应用中使用指纹身份验证的位置:
Android指纹图标
4、通知用户–应用程序应向用户显示某种形式的通知,表明指纹扫描仪处于活动状态并正在等待触摸或滑动。
IOS的Touch ID and Face ID
参考:Use Touch ID and Face ID with Xamarin.iOS
概述
iOS支持两种生物识别系统:
- Touch ID使用“主页”按钮下的指纹传感器。
- Face ID使用前置摄像头传感器通过面部扫描对用户进行身份验证。
iOS 7中引入了Touch ID,iOS 11中引入了Face ID。
这些身份验证系统依赖于称为Secure Enclave的基于硬件的安全处理器。 Secure Enclave负责加密面部和指纹数据的数学表示,并使用此信息对用户进行身份验证。 根据Apple的说法,面部和指纹数据不会离开设备,也不会备份到iCloud。
应用程序通过本地身份验证API与Secure Enclave进行交互,并且无法检索人脸或指纹数据或直接访问Secure Enclave。
在提供对受保护内容的访问权限之前,应用程序可以使用Touch ID和Face ID对用户进行身份验证。
本地身份验证上下文
iOS上的生物特征认证依赖于本地认证上下文对象,该对象是LAContext类的实例。 LAContext类使您可以:
- 检查生物识别硬件的可用性。
- 评估身份验证策略。
- 评估访问控制。
- 自定义并显示身份验证提示。
- 重用或使身份验证状态无效。
- 管理凭据。
您可以使用身份验证上下文,通过触摸识别或面部识别等生物识别技术或通过提供设备密码来评估用户的身份。 上下文处理用户交互,并且还连接到Secure Enclave,Secure Enclave是管理生物识别数据的基础硬件元素。 您创建并配置上下文,并要求其执行身份验证。 然后,您将收到一个异步回调,该回调提供身份验证成功或失败的指示,以及一个错误实例,该实例说明失败的原因(如果有)
更多参考:https://developer.apple.com/documentation/localauthentication/lacontext
检测可用的身份验证方法
该示例项目包括一个由AuthenticationViewController支持的AuthenticationView。 此类重写ViewWillAppear方法以检测可用的身份验证方法:
partial class AuthenticationViewController: UIViewController { // ... string BiometryType = ""; public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); unAuthenticatedLabel.Text = ""; var context = new LAContext(); var buttonText = ""; // Is login with biometrics possible? if (context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out var authError1)) { // has Touch ID or Face ID if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) { context.LocalizedReason = "Authorize for access to secrets"; // iOS 11 BiometryType = context.BiometryType == LABiometryType.TouchId ? "Touch ID" : "Face ID"; buttonText = $"Login with {BiometryType}"; } // No FaceID before iOS 11 else { buttonText = $"Login with Touch ID"; } } // Is pin login possible? else if (context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out var authError2)) { buttonText = $"Login"; // with device PIN BiometryType = "Device PIN"; } // Local authentication not possible else { // Application might choose to implement a custom username/password buttonText = "Use unsecured"; BiometryType = "none"; } AuthenticateButton.SetTitle(buttonText, UIControlState.Normal); } }
当UI将要显示给用户时,将调用ViewWillAppear方法。 此方法定义LAContext的新实例,并使用CanEvaluatePolicy方法确定是否启用了生物特征认证。 如果是这样,它将检查系统版本和BiometryType枚举,以确定哪些生物特征选项可用。
如果未启用生物特征认证,则该应用会尝试回退到PIN认证。 如果生物特征认证和PIN认证均不可用,则设备所有者尚未启用安全功能,并且无法通过本地认证来保护内容。
验证用户
示例项目中的AuthenticationViewController包含一个AuthenticateMe方法,该方法负责认证用户:
partial class AuthenticationViewController: UIViewController { // ... string BiometryType = ""; partial void AuthenticateMe(UIButton sender) { var context = new LAContext(); NSError AuthError; var localizedReason = new NSString("To access secrets"); // Because LocalAuthentication APIs have been extended over time, // you must check iOS version before setting some properties context.LocalizedFallbackTitle = "Fallback"; if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0)) { context.LocalizedCancelTitle = "Cancel"; } if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) { context.LocalizedReason = "Authorize for access to secrets"; BiometryType = context.BiometryType == LABiometryType.TouchId ? "TouchID" : "FaceID"; } // Check if biometric authentication is possible if (context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out AuthError)) { replyHandler = new LAContextReplyHandler((success, error) => { // This affects UI and must be run on the main thread this.InvokeOnMainThread(() => { if (success) { PerformSegue("AuthenticationSegue", this); } else { unAuthenticatedLabel.Text = $"{BiometryType} Authentication Failed"; } }); }); context.EvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason, replyHandler); } // Fall back to PIN authentication else if (context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out AuthError)) { replyHandler = new LAContextReplyHandler((success, error) => { // This affects UI and must be run on the main thread this.InvokeOnMainThread(() => { if (success) { PerformSegue("AuthenticationSegue", this); } else { unAuthenticatedLabel.Text = "Device PIN Authentication Failed"; AuthenticateButton.Hidden = true; } }); }); context.EvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, localizedReason, replyHandler); } // User hasn‘t configured any authentication: show dialog with options else { unAuthenticatedLabel.Text = "No device auth configured"; var okCancelAlertController = UIAlertController.Create("No authentication", "This device does‘t have authentication configured.", UIAlertControllerStyle.Alert); okCancelAlertController.AddAction(UIAlertAction.Create("Use unsecured", UIAlertActionStyle.Default, alert => PerformSegue("AuthenticationSegue", this))); okCancelAlertController.AddAction(UIAlertAction.Create("Cancel", UIAlertActionStyle.Cancel, alert => Console.WriteLine("Cancel was clicked"))); PresentViewController(okCancelAlertController, true, null); } } }
响应于用户点击登录按钮,调用AuthenticateMe方法。 实例化一个新的LAContext对象,并检查设备版本,以确定要在本地身份验证上下文上设置的属性。
调用CanEvaluatePolicy方法以检查是否启用了生物特征认证,并在可能的情况下退回PIN认证,如果没有可用的认证,则最终提供不安全的模式。 如果有身份验证方法可用,则使用EvaluatePolicy方法显示UI并完成身份验证过程。
该示例项目包含模拟数据和一个视图,如果身份验证成功,该视图将显示数据。
原文地址:https://www.cnblogs.com/peterYong/p/12434683.html