使用登录界面来保护APP用户数据是很好的方法--你可以使用Keychain(内嵌在iOS里的)来确保用户数据的安全。不过苹果现在使用Touch ID提供了另外一层保护,该功能适用于iPhone5、iPhone6、 iPhone 6+、iPad Air2以及iPad mini 3。
如果这些都还不够,可以尝试iOS 8引入的扩展,你甚至可以使用AgileBits开发的1Password app来整合登录信息的存储和获取。这一切都要感谢AgileBits团队开发者开源了他们的扩展包。这样你就可以把管理登录信息的责任交给Keychain、TouchID或者1Password。
在这个教程中,我们将使用Keychain 来存储和验证登录信息。之后,我们会学习Touch ID,最后将1Password 扩展集成到你的app中。
注意:Touch ID和1Password要求在真机上测试,Keychain可以在模拟器上测试。
开始
请在此下载该教程的初始工程文件。
这是一个最基本的使用Core Data存储用户笔记的记笔APP;storyboard中包含一个登录界面,用户可输入用户名和密码,这个APP的其他界面已经都关联好了,而且可以直接使用。
编译并运行工程,查看APP在当前状态下的展示情况:
现在,点击Login按钮会关闭当前界面并展示笔记的列表-你也可以在该界面上创建一个新的笔记。点击Logout会带你返回到Login界面。如果这个APP处于后台,它会马上返回到login界面。这种方式保障了在没有登录的情况下是看不到任何数据的。将Info.plist里面的Application does not run in background设置为YES就可以达到这个效果。
开始之前,更改Bundle Identifier,选择一个合适的Team。
在工程导航器中选中TouchMeIn,然后选择TouchMeIn target。在General 标签中更改Bundle Identifier为你自己的域名-使用反向域名构造标识符规则-例如com.raywenderich.TouchMeIn.
然后,如下,在Team菜单上选择你的开发团队相应的账户。
所有配置齐全后,开始coding吧!:]
Logging? Log In.
话不多说,开始吧!现在你要增加功能来对照用户提供的验证信息和硬编码的值。
打开LoginViewController.swift,在managedObjectContext变量声明的下方添加下面的常量:
1 2 |
|
以上是仅仅是硬编码用户名和密码,用来核对用户提供的验证信息。
在loginAction(_:)下方添加以下函数:
1 2 3 4 5 6 7 |
|
该方法查看用户提供的验证信息与你提前定义的常量是否匹配。
接下来,将loginAction(_:) 里的内容替换为如下内容:
1 2 3 |
|
该方法调用了checkLogin(_:password:)方法,如果验证信息正确则收起登录界面。
编译并运行程序,输入用户名batman和密码Hello Bruce!,点击Login按钮,登录界面应该按照预期被收起。
尽管这个做法可以达到验证效果,但是非常不安全,因为以字符串形式保存验证信息能被训练有素的黑客使用正确的工具轻松获取。最好的策略是,永远不要将密码直接保存在app里。
为此,你应该使用Keychain 来保存密码。查看Chris Lowe的Basic Security in iOS 5--Part 1教程来学习Keychain 的实际工作原理。
下一步将Keychain封装添加到你的APP。虽然它是用Objective-C写的,但也要添加一个bridging header头文件,以便从Swift中访问Objective-C类。
Rapper? No. Wrapper.
你可以在此下载KeychainWrapper,来自于苹果的Keychain Services Programming Guide.
下载解压后,将KeychainWrapper.h 和 KeychainWrapper.m拖进你的工程里,如下所示:
在弹出窗口中,选中Copy items if needed 和TouchMeIn target。
在Swift工程中添加Objective-C文件,Xcode会为你创建一个桥接头文件-点击 Yes:
这时候创建了一个名为TouchMeIn-Bridging-Header.h的桥接头文件。将所有Objective-C文件的头文件添加到文件中,以便Swift工程可以访问得到。
想要检查桥接头文件设置是否正确,在工程导航器中选择TouchMeIn。选中Build Settings,在搜索栏中输Swift Compiler,然后找到Objective-C Bridging Header栏,当前栏中包含TouchMeIn/TouchMeIn-Bridging-Header.h如下图所示:
提示:你可以在Porting Your App to the iPhone 6, iPhone 6 Plus and iOS 8: Top 10 Tips(中文译文)中了解更多关于桥接头文件的信息。想了解更多Swift和Objective-C混编的信息,可以查看苹果的Using Swift with Cocoa and Objective-C指南.
打开TouchMeIn-Bridging-Header.h文件,在文件开头导入Keychain 封装包:
1 |
|
编译并运行工程,以确保没有任何错误。一切正常?很好—现在你可以在APP中发挥Keychain的优势了。
提示:如果工程中存在错误,请查看苹果的Using Swift with Cocoa and Objective-C指南来修复错误。
Keychain, Meet Password. Password, Meet Keychain
想使用Keychain,必须先保存一个用户名和密码。然后检查用户提供的验证信息和keychain里面保存的信息是否匹配。
你应该追踪用户是否已经创建了验证信息,以便你可以把login按钮的展示文字从“Create”更改为“Login”。你也应该存储用户名到user defaults当中,以便检查验证信息是否创建,而不是每次都访问keychain。
打开LoginViewController.swift文件然后删掉以下内容:
let usernameKey = "batman"
let passwordKey = "Hello Bruce!"
在以上删除掉的地方添加下面的内容:
1 2 3 4 |
|
MyKeychainWrapper保留了到Objective-C KeychainWrapper类的引用。接下来的两个常量会用来区分Login按钮到底是用来创建验证信息还是用来登录;根据前面的两个状态(创建或登录),LoginButton outlet用来更新login 按钮的展示文字。
打开Main.storyboard,然后从Login View Controller里执行Ctrl-drag操作,拖拉到Login按钮,如图所示:
在弹出框里选择loginButton
接下来,当按钮被轻触时,你要处理两种可能情况:如果用户还没有创建过验证信息时,按钮文字应该展示“Create,否则按钮展示“Login”。你也需要检查输入的验证信息和keychain保存的信息是否匹配。
打开LoginViewController.swift,将loginAction(_:) 里面的代码替换为如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
上述代码分析:
代码里发生了如下步骤:
- 如果用户名或密码为空,则弹出一个提示框并从该方法返回。
- 如果键盘可见则关闭它。
- 如果login 按钮的tag是createLoginButtonTag,则继续创建一个新的login。
- 接下来,从NSUserDefaults里读取hasLoginKey 的值,该值表明Keychain里面是否已经保存过了密码。如果username非空,而且hasLoginKey 表明没有保存过登录信息,则将username的值保存到NSUserDefaults。
- 然后使用mySetObject和writeToKeychain把密码的值保存在Keychain中。之后将NSUserDefaults里面hasLoginKey的值设置为true,用以表示密码已经被保存在了keychain当中了。设置login按钮的tag值为loginButtonTag ,这样用户下次开启你的APP时会自动弹出让用户登录的界面,而不是弹出让用户创建登录的界面。最后,关闭loginView。
- 如果用户是登录(如loginButtonTag表明),可调用checkLogin(_:password:)方法来验证用户提供的登录信息;如果验证信息是匹配的,则关闭登录界面。
- 如果验证失败,则弹出提示信息给用户。
注意:为什么不直接像用户名那样把密码直接保存在NSUserDefaults里面呢?因为直接保存后果很严重。因为NSUserDefaults是由plist文件存储的。Plist文件本质上就是一个在APP的Library文件夹下的一个XML文件 ,它可以被任何可直接接触到设备的任何人读取。另一方面,Keychain则是利用Triple Digital Encryption Standard (3DES) 来加密数据的。
接下来,用以下内容替换checkLogin(_:password:)方法的实现:
1 2 3 4 5 6 7 8 |
|
改方法检查了用户名是否匹NSUserDefaults里储存的值,以及密码是否匹配Keychain里存的值。
现在就需要根hasLoginKey的状态来设置正确的按钮展示文字以及tag。
将以下内容添加到viewDidLoad()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
代码解释如下:
- 首先利用hasLoginKey的值来查看当前用户是否已经保存过登录信息。
- 如果保存过,把登录按钮的文字设为Login,更新按钮的tag值为loginButtonTag,并隐藏createInfoLabel--包含“Start by creating a username and password”文本。如果该用户没有保存过登录信息,则设置按钮文字为Create,且展示createInfoLabel。
- 最后,把用户名信息填入NSUserDefaults里以便用户更方便的进行登录操作 ?
编译并运行。输入一个你选择的用户名和密码,点击Create。
注意:如果你忘记了把loginButton IBOutlet关联起来,则可能会看见“Fatal error: unexpectedly found nil while unwrapping an Optional value”。如果看见了这个错误信息,则照先前步骤提示将outlet关联起来。
现在点击Logout,然后尝试用相同的用户名和密码进行登录-应该就能看到一串笔记列表。
点击Logout再次登录,这次使用另外的一个密码,后点Login,你应该会看到如下的错误提示框:
恭喜!你已经成功利用Keychain进行身份验证啦。下一步,挑战Touch ID吧!
Touching You, Touching Me
注意:想要尝试Touch ID,你必须在一个支持Touch ID的真机上运行APP。目前为止,支持的设备有iPhone 5s/6/6+、iPad Air 2以及iPad mini 3。
在这节中,除了使用Keychain外,你还要在工程中加入Touch ID。尽管Touch ID工作时并非一定要使用Keychain ,但保守起见最好还是实现一个备用的认证方法,例如Touch ID运行失败,或者用户手机不支持Touch ID。
打开 Images.xcassets.
在这里下载该工程将使用到的图片资源。解压并打开文件夹,找到Touch-icon-lg.png、[email protected]和[email protected], 选中这三张图片然后拉到Images.xcassets下。对于Xcode,这三张图片是同一张图片,只是分辨率不同而已。
打开Main.storyboard,然后从Object Library中拖一个按钮到Login View Controller Scene,位于标签下方。
按照下面操作,使用Attributes Inspector来调节按钮的属性:
- 将Type设置为Custom.
- 将Title设置为空.
- 将Image设置为Touch-icon-lg.
完成后,按钮的属性值应该如下图所示:
按照如下值在Size Inspector中设置按钮:
- Show 设为 Frame Rectangle
- X 设为 267
- Y 设为 341
- Width 设为 67
- Height 设为 66
完成后,Size Inspector应该如下图:
选中你创建的这个新按钮,点击storyboard画布下方的layout bar当中的pin按钮,按照如下信息设置约束条件:
- Top Space 设为 16.5
- Width 设为 67
- Height 设为 67
接下来,点击align 按钮并在Container中检查Horizontal Center。
最后,点击Resolve Auto Layout Issues图标,选中Selected Views\Update Frames,如下所示:
这会更新按钮的frame以便匹配新的约束。
你的界面现在应该如下图:
下一步,依然在Main.storyboard中,打开 Assistant Editor,确保已经展示LoginViewController.swift。
按住Ctrl键把新建的按钮关联到LoginViewController.swift,放置在其他的属性下方,如下图:
在弹出框中将其命名为touchIDButton,然后点击Connect。
当设备不支持Touch ID时,这会创建一个outlet用来隐藏这个按钮。
现在给这个按钮加一个action。
按住Ctrl键拖拽这个按钮到 LoginViewController.swift 里的checkLogin(_:password:)方法上方:
在弹出框里,将Connection更改为Action,将其命名为touchIDLoginAction,然后点击Connect。
编译并运行看是否存在错误。目前仍然可以选择模拟器运行,因为还没有加任何关于Touch ID的东西。但是下面就要开始引入了。
Adding Local Authentication
实现Touch ID就跟引入Local Authentication和调用一些便捷却强大的方法一样简单。
以下是Local Authentication文档内容:
“Local Authentication 框架提供了基于特定安全策略的向用户要求身份验证的工具。”
这里提到的特定安全策略就是用户的生物识别信息,也就是用户的指纹。
打开LoginViewController.swift,在CoreData import下加入下面的import
1 |
|
现在需要一个指向LAContext 类的引用,还需要一个error属性。
将以下代码加入所有属性的下方:
1 2 |
|
接下来在这个教程中,你将会用到error;context引用了一个验证上下文环境,也就是 Local Authentication中的主要元素。
在viewDidLoad() 方法底部加入下面的代码:
1 2 3 4 5 |
|
这里你使用了canEvaluatePolicy(_:error:)方法来查看当前设备是否支 Touch ID 身份验证。如果支持,则显示 Touch ID 按钮;如果不支持,则隐藏按钮。
在模拟器上编译运行示例工程,Touch ID按钮是隐藏的。在支持Touch ID的真机上运行,Touch ID按钮则会显示出来。
使用Touch ID
还是在LoginViewController.swift中,用以下代码替换touchIDLoginAction(_:)方法里的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
以下是上面代码的解释:
再一次使用canEvaluatePolicy(_:error:) 来检查当前设备是否支Touch ID。
如果该设备支持Touch ID, 使用 evaluatePolicy(_:localizedReason:reply:) 方法来开始验—也就是让用户利用Touch ID进行身份验证。在验证执行完毕后,evaluatePolicy(_:localizedReason:reply:) takes一个reply block
在reply block内部, 首先处理成功的情况。默认情况下, 验证方法在私有线程上运行,所以代码会跳到主线程以便更新UI。如果验证成功,则调用segue关闭登录界面。
如果失败,用switch语句把每一个可能的失败条件列举出来,并设置相应的失败提示信息展示给用户。
如果canEvaluatePolicy(_:error:)方法失败,则展示一个通用的提示框。实际应用中, 应该分析评估并解决返回的失败错误代码,错误码可能包含以下的信息:
- LAErrorTouchIDNotAvailable: 该设备不支持Touch ID。
- LAErrorPasscodeNotSet:Touch ID要求的passcode没启用。
- LAErrorTouchIDNotEnrolled: 没有存储指纹信息。
iOS会对LAErrorPasscodeNotSet和LAErrorTouchIDNotEnrolled 做出自己相对应的警告提示框。
在真机上编译运行,然后用Touch ID测试登录。
因为LAContext 已经处理了大多数较难的部分,所以实现Touch ID其实是个相对简单的过程。好的地方是,你可以在一个APP里保留keychain和Touch ID两个验证方法以防用户的手机不支持Touch ID。
在这个教程的第三部分也就是最后一个部分,我们将会使用1Password 的iOS 8扩展来存储并获得登录信息,以及其他的一些敏感的用户信息。
用1Password进行控制
Agilebits开发的1Password密码控制器可以在iOS、OS X、Windows以及Android上运行。它可以把登录验证信息,软件许可证和其他敏感信息存储在一个库里并且用一个PBKDF2-加密的主秘钥进行锁定。在这一节中,你会学到怎么样利用扩展(extension)把用户验证信息存储在1Password里,然后学习怎样获取这些信息以便验证用户。
注意:下面章节学习中,你需要在真机上安装1Password。
首先,你需要加一些新的图片,将用于1Password按钮上。
打开 Images.xcassets. 然后,在你先前下载的资源里,找到下面的三个文件:
- onepassword-button.png
- [email protected]
- [email protected]
选中所有,然后把三个当成一个整体拖入到 Images.xcassets. 然后,找到这三个文件:
- onepassword-button-green.png
- [email protected]
- [email protected]
再次,选中所有,然后把三个当成一个整体拖入到 Images.xcassets.
打开Main.storyboard,从Object Library里面拖一个按钮到Login View Controller Scene里,位于的Touch ID按钮下方。在Attributes Inspector里,调整这个按钮的属性为如下值:
- 将Type设置为Custom
- 将Title设置为空
- 将Image设置为onepassword-button
Attributes Inspector应该如下图所示:
使用Size Inspector调整放置信息和大小信息,如下值:
- 将X设置为287
- 将Y设置为426
- 将Width设置为27
- 将Height设置为33
你可以比对下图来检查大小信息和位置信息是否设置正确:
保持按钮仍然是被选中状态,点击storyboard下方layout条上pin,并且设置按钮的距离上部空间为21,宽度为27,高度为33:
接下来,点击layout条中的align,并在Container里面勾选上Horizontal Center。
还是在Main.storyboard里,打开 Assistant Editor,当下会自动打开LoginViewController.swift-如果没有打开,则在跳转栏中选中这个文件。现在按住Ctrl键把按钮拖向LoginViewController.swift中的其他属性下方,如下图所示:
在弹出框里把名字设置为onepasswordSigninButton,点击Connect:
这会创建一个IBOutlet,根据其功能是否可用,你将用它来更改1Password按钮的图片。
接下来为onepasswordSigninButton添加一个action。按Ctrl健并把该按钮拖向LoginViewController.swift里的checkLogin(_:password:)方法上方。
将Connection类型更改为Action。将名称设置为canUse1Password,且将Arguments设置为Sender,点击Connect。
编译运行。如下图,你会看到一个新的1Password按钮展现出来,如下所示:
一个灵活的拓展
现在界面展示出来了,你可以开始实现1Password的支持了!
如下展示的那样,找到GitHub上的1Password 扩展仓库地址,点击下载ZIP:
解压下载下来的文件。打开文件夹并且把OnePasswordExtension.h和OnePasswordExtension.m文件拖到你的工程里,如下图:
确保勾选Copy items if needed和TouchMeIn。
回想当初添加Objective-C 文件到Swift工程里时,Xcode自动提供了一个bridging header头文件。鉴于最初时已经创建了这样的一个bridging header头文件,现在你可以直接将1Password的头加入那个文件中。
打开TouchMeIn-Bridging-Header.h文件然后加入下面的import:
1 |
|
打开LoginViewController.swift然后加入下面import到文件的顶端:
1 |
|
这仅仅简单地导入了1Password 扩展需要的Security框架。
现在将以下属性添加至LoginViewController的顶端:
1 2 |
|
前一个属性保留了1Password扩展的主类的关联,后一个属性追踪了1Password记录是否被创建过;一开始设置默认为false因为在最开始运行时肯定没有创建过。
接下来,使用以下代码更新viewDidLoad()里整个if halogen 块里的代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
这使得onepasswordSigninButton在初始用户名和密码存储进Keychain之前是无效的。
现在你需要进入1Password扩展里测试是否安装了这个iOS app以及其是否可用,如果是就启用1Password按钮。
将以下代码加入viewDidLoad()方法:
1 2 3 4 5 6 7 8 9 10 |
|
这里默认是隐藏了onepasswordSigninButton,并且仅当扩展被安装后才显示它。接下来将NSUserDefaults里面key has1PassLogin的值赋值给has1Password来指明1Password记录是否被创建过。
接下来,当点1Password按钮时,更改canUse1Password以便向console输出一个简单的消息。
1 2 3 |
|
在已安装1Password的模拟器和真机上编译和运行你的APP。1Password按钮应该在模拟器上是隐藏的,且在真机上该按钮应该显示为绿色。点击1Password按钮,以下信息将显示在console上:
1 |
|
深入1Password强大之处
1Password扩展里有一些方法你可以使用:
storeLoginForURLString(_:loginDetails:passwordGenerationOptions:forViewController:sender:)方法让你创建一些登录验证信息,findLoginForURLString(_:forViewController:sender:completion:)方法能让你从1Password库里检索验证信息。
打开LoginViewController.swift,并添加以下新方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
以上是代码,下面是代码的细节:
- 创建了一个包含用户提供的用户名和密码的dictionary,也包括1Password扩展需要的keys。当存储密码时,这个dictionary会被传递给扩展。
- 添加了1Password用来生成密码的可选的参数。这里只是简单地声明密码的最小和最大长度。如果不在这里配置的话,1Password扩展会提供默认的参数值。
- 你提供一个字符串--TouchMeIn.Login--来确定保存了的记录; 你也要传递newLoginDetails和passwordGenerationOptions,这是你在步骤1和步骤2创建的两个字典。
- 如果上述一切顺利,你会收到一个loginDict返回值。这个返回值包含了用户名和密码;如果没收到,你需要在console里打印一个错误然后return。
- 从loginDict 里提取出用户名和密码。
- 利用上一步中拿到的用户名和密码来调用 checkLogin(_:password:) 方法;如果验证信息跟存储在Keychain里的信息匹配则该用户登录成功且关闭login界面;
- 如果登录信息不正确,你展示一个带有错误信息的提示框。
注意:1Password使用一个字符串--URLString作为库里记录对应的key。你可以使用任何独一无二的字符串,尽管使用你的Bundle ID作为这个Key是很诱人的,但是你还是应该尽量创造一个唯一的可读性强的字符串来当做key,因为这个字符串在1Password app里是可见的。在你的案例中,你使用了TouchMeIn.Login这个字符串。
现在你应该检查用户名是否被存入了NSUserDefaults。如果没有,你应该保守的编写代码,把text field里面的用户名值保存下来 ?
在saveLoginTo1Password(_:)里找到下面的这行代码:
1 |
|
然后用下面的代码替换它:
1 2 3 |
|
在用户名没有被保存的情况下,这将把用户名输入框里的值赋值给user defaults里的key值为username的变量。
在saveLoginTo1Password(_:)方法的下端添加下面的代码:
1 2 |
|
这会更新 has1PassLogin 以便表明该用户已经创建了一个1Password的记录。这是一个简单的方法来避免在1Password库中创建第二次记录。后面,你将在试图保存用户验证信息到1Password之前检 has1PassLogin 。
现在你需要一个方法为你的APP查看已存储的1Password登录信息。
在checkLogin(_:password:)上方添加下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
这个方法使用 1Password的findLoginForURLString(_:forViewController:sender:completion:)方法来查看传进来URLString值对应的库中的记录,在这个例子里就是TouchMeIn.Login;然后查找成功后会运行completion block,里面包含了返回的字典。
进一步解析代码如下:
- 如果 loginDict 是 nil ,那就是出错, 你将根据提供的NSError console里打印错误信息,然后return。????
- 现在你检查了用户名是否保存在了NSUserDefaultsIf里。如果没有,则根据1Password的返回值保存用户名。
- 把得到的值传递给 checkLogin(_:password:)方法。如果登录信息跟Keychain里存储的信息匹配,则关闭登录界面。否则,如果不匹配,给用户展示一个提示框。
现在你将通过调用saveLoginTo1Password(_:)方法或者findLoginFrom1Password(_:)方法结束canUse1Password(_:)方法的实现。如果第一次运行则调用saveLoginTo1Password(_:)方法,如 has1PassLogin变量表明该用户曾经有过保存的登录信息,则调用findLoginFrom1Password(_:)方法。
利用下面的代码替换canUse1Password的实现:
1 2 3 4 5 6 7 |
|
编译并运行。点击1Password按钮来保存你的登录信息。1Password的icon会出现在Action表单,点击展开扩展,扩展的视图会模态地出现。你可以用系统密码登录,或者如果使用的是真机,则可使用Touch ID。1Password然后会展示你的APP的记录,使用你先前设置好的标识符。
你将会看到如下展示的界面。默认情况下1Password会生成一个密码。在这个测试当中,确保输入的验证信息跟先前用的Keychain的登录信息一样。 等到那个结束后,点击Done按钮,最后一张截图展示了稍后编辑记录时你会看到的-它有库名、URL以及当创建记录时你提供的笔记。
现在你已经存储了登录,随后点击1Password的按钮会获取库的验证信息,而且当这个登录信息跟Keychain里的匹配时会允许登录。试着再次登录你将会看到如下界面:
好了-你现在已经完成了1Password扩展支持- 一个强大的方法来增1Password用户的登录体验。
接下来做什么?
你可以在这里下载教程的完整示例应。
这个教程里你所创建的LoginViewController为任何需要管理用户登录信息的APP提供了一个出发点。
你也可以添加一个新的view controller,或者改变已有的LoginViewController来允许用户时不时地改变他们的密码。这里Touch ID不是必须的,因为用户的生理信息可能在整个生命周期中都不会改变多少! 但是,你可以新建一个方法来更新Keychain然后相应的更新1Password;你应该在接受用户更新密码之前促使用户输入当前的密码。
原文地址:http://www.cocoachina.com/ios/20150313/11241.html