Swift保存RSA密钥到Keychain
作者:互联网
https://www.jianshu.com/p/c1e9bffc76f4
最近项目的需求用到RSA的加密解密,并且需要把公钥信息保存到Keychain里面,网上很多文章都是用Keychain保存账号密码什么的,自己在实现保存的公钥过程中也踩了不少坑。现在来整理一下实现的方法。
首先肯定是先要导入 Security
import Security
然后定义一个常量 Identifier
// 密钥唯一标示
fileprivate let publicKeyIdentifier = "com.hhh.publicKey"
fileprivate let privateKeyIdentifier = "com.hhh.privateKey"
fileprivate let publicKeyTag = publicKeyIdentifier.data(using: .utf8)!
fileprivate let privateKeyTag = privateKeyIdentifier.data(using: .utf8)!
实现一个把SecKey转换成Data的方法
private static func getKeyDataFrom(secKey: SecKey, tag: Data) -> Data {
var data: Data?
var query = [String: Any]()
query[kSecClass as String] = kSecClassKey
query[kSecAttrApplicationTag as String] = tag
query[kSecAttrKeyType as String] = kSecAttrKeyTypeRSA
var attributes = query
attributes[kSecValueRef as String] = secKey
attributes[kSecReturnData as String] = true
var result: CFTypeRef?
let status = SecItemAdd(attributes as CFDictionary, &result)
if status == errSecSuccess {
data = result as? Data
SecItemDelete(query as CFDictionary)
}
return data!
}
这个方法的原理其实就是先把SecKey存到Keychain里面然后再读取出Data类型的数据
接下来就是实现存到Keychain的方法
// keySize 就是RSA密钥的长度
// isPrivate 判断存储的是否为私钥(true为私钥、false为公钥)
static func saveRSAKeyToKeychain(key: SecKey, keySize: size_t, isPrivate: Bool) {
var saveDictionary = [String: Any]()
let keyClass = isPrivate ? kSecAttrKeyClassPrivate : kSecAttrKeyClassPublic
// 设置keychain字典
saveDictionary[kSecClass as String] = kSecClassKey
saveDictionary[kSecAttrKeyType as String] = kSecAttrKeyTypeRSA
saveDictionary[kSecAttrApplicationTag as String] = isPrivate ? privateKeyTag : publicKeyTag
saveDictionary[kSecAttrKeyClass as String] = keyClass
saveDictionary[kSecValueData as String] = getKeyDataFrom(secKey: key, tag: isPrivate ? privateKeyTag : publicKeyTag)
saveDictionary[kSecAttrKeySizeInBits as String] = keySize
saveDictionary[kSecAttrEffectiveKeySize as String] = keySize
saveDictionary[kSecAttrCanDerive as String] = kCFBooleanFalse
saveDictionary[kSecAttrCanEncrypt as String] = kCFBooleanTrue
saveDictionary[kSecAttrCanDecrypt as String] = kCFBooleanTrue
saveDictionary[kSecAttrCanVerify as String] = kCFBooleanTrue
saveDictionary[kSecAttrCanSign as String] = kCFBooleanFalse
saveDictionary[kSecAttrCanWrap as String] = kCFBooleanTrue
saveDictionary[kSecAttrCanUnwrap as String] = kCFBooleanFalse
saveDictionary[kSecAttrApplicationLabel as String] = isPrivate ? privateKeyIdentifier : publicKeyIdentifier
// 删除旧数据
SecItemDelete(saveDictionary as CFDictionary)
let status = SecItemAdd(saveDictionary as CFDictionary, nil)
assert(status == errSecSuccess, "keychain存储密钥失败")
}
最后就是实现从Keychain取出密钥信息的方法
// isPrivate 判断存储的是否为私钥(true为私钥、false为公钥)
static func getRSAKeyFromKeychain(isPrivate: Bool) -> SecKey! {
var queryDictionary = [String: Any]()
let keyClass = isPrivate ? kSecAttrKeyClassPrivate : kSecAttrKeyClassPublic
queryDictionary[kSecClass as String] = kSecClassKey
queryDictionary[kSecAttrKeyType as String] = kSecAttrKeyTypeRSA
queryDictionary[kSecAttrApplicationTag as String] = isPrivate ? privateKeyTag : publicKeyTag
queryDictionary[kSecAttrKeyClass as String] = keyClass
queryDictionary[kSecReturnRef as String] = kCFBooleanTrue
queryDictionary[kSecAttrApplicationLabel as String] = isPrivate ? privateKeyIdentifier : publicKeyIdentifier
var key: CFTypeRef?
let status = SecItemCopyMatching(queryDictionary as CFDictionary, &key)
if status == errSecSuccess {
// 强转SecKey类型
return key as! SecKey
}
assert(false, "keychain读取密钥失败")
}
因为在开头我们已经定义了公钥私钥的 Identifier 所有我们只需要判断存/取的是私钥还是公钥就可以将密钥信息进行读写操作。
作者:小天枢丶
链接:https://www.jianshu.com/p/c1e9bffc76f4
Swift语言操作KeyChain
https://blog.csdn.net/liulegend/article/details/515045922016年05月26日 09:53:28 liulegend 阅读数:3093
关于利用KeyChain保存信息到iphone上,网上有一些信息,但往往有各种问题。尤其是更新KeyChain信息时,常常遇到错误代码-50。苹果在对开发者的支持力度上与微软的差距不是一般的大。还好有stackoverflow.com这个平台,可以看到一些其他人碰到的类似问题和处理方式,虽然没有找到满意的答复,但参考多方的代码总算把程序调通了。现分享如下:
1、引用Security:
import Security
2、添加KeyChain:
var addQuery : [NSString : AnyObject] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : "MyService",
kSecAttrLabel : "My UUID",
kSecAttrAccount : "MyUsername",
kSecValueData : "12345678".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)!
]
let result = SecItemAdd(addQuery, nil)
if result == errSecSuccess {
//操作成功处理
}
else {
//操作失败处理
}
3、获取KeyChain内容
var requestQuery : [NSString : AnyObject] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : "MyService",
kSecReturnAttrbutes : true,
kSecReturnData : true
]
var requestResult : AnyObject?
let err = SecItemCopyMatching(requestQuery, &requestResult)
if err == errSecSuccess {
if let results = requestResult as? [NSString : AnyObject],
let username = results[lSecAttrAccount] as? String,
let passdata = results[kSecValueData] as? NSData,
let password = NSString(data:passdata, encoding:NSUTF8StringEncoding) as? String {
//获取成功后的处理
}
}
else if err == errSecItemNotFound {
//未获取到信息的处理
}
else {
//其他错误的处理
}
4、更新KeyChain
var updateQuery : [NSString : AnyObject] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : "MyService",//非必需,建议加上
kSecAttrLabel : "My UUID",//非必需,建议加上
kSecAttrAccount : "MyUsername"//非必需,建议加上
]
var updateValue : [NSString : AnyObject] = [
kSecValueData : "87654321".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)!
]
let err = SecItemUpdate(updateQuery, updateValue)
if err == errSecSuccess {
//成功处理
}
else {
//失败处理
}
5、删除KeyChain
var query : [NSString : AnyObject] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : "MyService"
]
let status = SecItemDelete(query)
if status == errSecSuccess {
//删除成功处理
}
else {
//删除失败处理
}
以上代码在Xcode7.3.1及iphone 6 plus 9.3.2上验证成功。
82-Swift之密码管理(Keychain)的介绍和使用
https://www.jianshu.com/p/31e5654166db
2017.11.21 14:25 字数 505 阅读 1309评论 1喜欢 1
引言
App的开发中,用户登录的模块是必不可少的。用户登录一次之后,就可以使用自动登录功能。自动登录需要知道用户的账号和密码。那么用户的账户和密码是保密性的,为实现App的自动登录功能,我们要稳妥的管理好用户的账号和密码的安全。所以 Keychain 就被使用的上了。下面将给你介绍 Keychain 是什么、如何使用等。友情链接:Object-C 版的密码管理 Keychain 的连接地址 iOS的密码管理系统 Keychain的介绍和使用。
Keychain 是什么?
Keychain 是苹果公司 Mac OS(也包含 Mac OSX) 中的密码管理系统。
Keychain 的作用是什么?
Keychain 可以包含许多种类型的数据:密码(包括网站、FTP服务器、SSH账户、网络共享、无线网络、群组软件、加密磁盘镜像),私钥,电子证书、加密笔记等。
Keychain 的四个方法的介绍
1、数据的存储方法
@available(iOS 2.0, *)
public func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CoreFoundation.CFTypeRef?>?) -> OSStatus
@ attributes : 要存储的数据。
@ result : 存储数据后,返回的指向该数据的引用。
2、根据查询条件获取数据
@available(iOS 2.0, *)
public func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer<CoreFoundation.CFTypeRef?>?) -> OSStatus
@ query : 获取数据的查询条件。
@ result : 查询后获取到数据的引用。
3、更新数据
@available(iOS 2.0, *)
public func SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -> OSStatus
@ query : 要更新数据的查询条件。
@ attributesToUpdate : 要更新的数据内容。
4、删除数据
@available(iOS 2.0, *)
public func SecItemDelete(_ query: CFDictionary) -> OSStatus
@ query : 删除数据的查询条件。
总结,以上四个方法,就是 Keychain 的增、删、改、查的四个基本方法。这四个方法基本满足大部分App的使用需求。
Keychain 的整理和封装为一个类: KeychainManager.swift 。
KeychainManager.swift 的完整代码展示:
//
// KeychainManager.swift
// KeyChain
//
// Created by MAC on 2017/11/20.
// Copyright © 2017年 NetworkCode小贱. All rights reserved.
//
import UIKit
class KeychainManager: NSObject {
// TODO: 创建查询条件
class func createQuaryMutableDictionary(identifier:String)->NSMutableDictionary{
// 创建一个条件字典
let keychainQuaryMutableDictionary = NSMutableDictionary.init(capacity: 0)
// 设置条件存储的类型
keychainQuaryMutableDictionary.setValue(kSecClassGenericPassword, forKey: kSecClass as String)
// 设置存储数据的标记
keychainQuaryMutableDictionary.setValue(identifier, forKey: kSecAttrService as String)
keychainQuaryMutableDictionary.setValue(identifier, forKey: kSecAttrAccount as String)
// 设置数据访问属性
keychainQuaryMutableDictionary.setValue(kSecAttrAccessibleAfterFirstUnlock, forKey: kSecAttrAccessible as String)
// 返回创建条件字典
return keychainQuaryMutableDictionary
}
// TODO: 存储数据
class func keyChainSaveData(data:Any ,withIdentifier identifier:String)->Bool {
// 获取存储数据的条件
let keyChainSaveMutableDictionary = self.createQuaryMutableDictionary(identifier: identifier)
// 删除旧的存储数据
SecItemDelete(keyChainSaveMutableDictionary)
// 设置数据
keyChainSaveMutableDictionary.setValue(NSKeyedArchiver.archivedData(withRootObject: data), forKey: kSecValueData as String)
// 进行存储数据
let saveState = SecItemAdd(keyChainSaveMutableDictionary, nil)
if saveState == noErr {
return true
}
return false
}
// TODO: 更新数据
class func keyChainUpdata(data:Any ,withIdentifier identifier:String)->Bool {
// 获取更新的条件
let keyChainUpdataMutableDictionary = self.createQuaryMutableDictionary(identifier: identifier)
// 创建数据存储字典
let updataMutableDictionary = NSMutableDictionary.init(capacity: 0)
// 设置数据
updataMutableDictionary.setValue(NSKeyedArchiver.archivedData(withRootObject: data), forKey: kSecValueData as String)
// 更新数据
let updataStatus = SecItemUpdate(keyChainUpdataMutableDictionary, updataMutableDictionary)
if updataStatus == noErr {
return true
}
return false
}
// TODO: 获取数据
class func keyChainReadData(identifier:String)-> Any {
var idObject:Any?
// 获取查询条件
let keyChainReadmutableDictionary = self.createQuaryMutableDictionary(identifier: identifier)
// 提供查询数据的两个必要参数
keyChainReadmutableDictionary.setValue(kCFBooleanTrue, forKey: kSecReturnData as String)
keyChainReadmutableDictionary.setValue(kSecMatchLimitOne, forKey: kSecMatchLimit as String)
// 创建获取数据的引用
var queryResult: AnyObject?
// 通过查询是否存储在数据
let readStatus = withUnsafeMutablePointer(to: &queryResult) { SecItemCopyMatching(keyChainReadmutableDictionary, UnsafeMutablePointer($0))}
if readStatus == errSecSuccess {
if let data = queryResult as! NSData? {
idObject = NSKeyedUnarchiver.unarchiveObject(with: data as Data) as Any
}
}
return idObject as Any
}
// TODO: 删除数据
class func keyChianDelete(identifier:String)->Void{
// 获取删除的条件
let keyChainDeleteMutableDictionary = self.createQuaryMutableDictionary(identifier: identifier)
// 删除数据
SecItemDelete(keyChainDeleteMutableDictionary)
}
}
上面展示的代码已经注释非常详细了,就不在多介绍了。如果还有不了解的地方可以查看iOS版本的Keychain,连接如下:iOS的密码管理系统 Keychain的介绍和使用。
KeychainManager.swift 类的测试和使用
1、测试代码
// 存储数据
let saveBool = KeychainManager.keyChainSaveData(data: "我期待的女孩" as Any, withIdentifier: KeyChain)
if saveBool {
print("存储成功")
}else{
print("存储失败")
}
// 获取数据
let getString = KeychainManager.keyChainReadData(identifier: KeyChain) as! String
print(getString)
// 更新数据
let updataBool = KeychainManager.keyChainUpdata(data: "眼睛像云朵", withIdentifier: KeyChain)
if updataBool {
print("更新成功")
}else{
print("更新失败")
}
// 获取更新后的数据
let getUpdataString = KeychainManager.keyChainReadData(identifier: KeyChain) as! String
print(getUpdataString)
// 删除数据
KeychainManager.keyChianDelete(identifier: KeyChain)
// 获取删除后的数据
let getDeleteString = KeychainManager.keyChainReadData(identifier: KeyChain)
print(getDeleteString)
2、测试结果的展示
DAB86C95-248D-45B7-B9B6-D748DB7F9BE1.png
下载Demo 的方法
- 联系简主 m: 18801210281 q:1542100658
- 加入群:185341804
- email : zhoushaungjian511@163.com
iOS Keychain,SSKeychain,使用 理解 原理
我的邮件:m4email@163.com 如果有这篇文章对您有帮助就点下推荐或者随意评论一个呗,谢谢谢谢,随便转载,标明出处就好。
Keychain 使用? ---为了实用最大化我觉得我应该直接先说使用!
当然是使用第三方库啦:sskeychain 3000+星星的库不开玩笑。github地址:https://github.com/soffes/sskeychain
导入完之后首先,编译一下有无错。
如果是自己手动导入:
1.把SSKeychain.h SSKeychain.m SSKeychainQuery.h SSKeychainQuery.m 复制到工程
2.添加Security.framework 怎么添加?点一下那个+
3.SSKeychain.h有错?把SSKeychain.h 中的#import <SSKeychain/SSKeychainQuery.h> 换成 #import <Foundation/Foundation.h> #import "SSKeychainQuery.h" 吧。
还有错?作为小白我的也不知道了,发我邮件一起讨论吧。
接下来演示4个过程
基本说明:储存的数据有三个 1.服务名(这个方便对账号密码进行分类)2.账号3.密码 而这三个数据都是NSString (如果要存其他类型呢,请看后面吧)
所用到的API :
添加和更新都用这个: + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account ;
查询密码:+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
删除:+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
1.添加一条钥匙 (这个钥匙的信息 由 服务名+账号+密码 组成)
记得添加头文件
#import "SSKeychain.h"
#import "SSKeychainQuery.h"
//先定义一下要用的东东
NSString *serviceName= @"com.keychaintest.data";
NSString *account = @"m4abcd";
NSString *password = @"12345678";
//加入钥匙串!
if ([SSKeychain setPassword:password forService:serviceName account:account]) {
NSLog(@"success !");
}
说明:就是这么简单咯。
2.查询
1.查询某service 下 count 的密码并且打印出来:
NSLog(@"%@",[SSKeychain passwordForService:serviceName account:account]);
2.查询service下所有钥匙:
NSArray *keys = [SSKeychain accountsForService:serviceName];
这是我的输出:
2016-03-04 15:08:43.785 keychaintest[31342:4403403] (
{
acct = m4abcd;
agrp = test;
cdat = "2016-03-03 07:10:58 +0000";
mdat = "2016-03-04 07:08:43 +0000";
pdmn = ak;
svce = "com.keychaintest.data";
sync = 0;
tomb = 0;
}
)
说明:返回的结果为数组,数组成员就是我们查询的钥匙,这里只有一个钥匙,而钥匙信息以字典的形式构建的,键acct 就是count,键svce 就是serviceName。密码在哪里?用方法1去取吧骚年!
3.查询本appkeychain的所有钥匙
NSArray *keys = [SSKeychain allAccounts];
3.更新
if([SSKeychain setPassword:@"321321" forService:serviceName account:account]){
NSLog(@"set success!");
}
4.删除
if([SSKeychain deletePasswordForService:serviceName account:account]){
NSLog(@"delete success!");
}
说明:删除就是把这一条钥匙删除哦,不是只删除密码!
另外的说明:如果你的password 是NSData
查询: + (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
设置or更新:+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
下面开始浅浅的理解还有对苹果API进行一点点说明吧
1.Keychain 是什么?
keychain 就是放钥匙柜子!就是苹果提供给我们的一个保险柜。
这篇文章仅针对iOS。
在iOS中每个APP 都有属于自己的Keychain,最常用就是保存用户的账户和密码,就是记住密码,放在这里很安全(苹果负责帮我们加密再存起来,如果出了问题怪他咯!),假如用NSUserDefault 保存这些秘密数据,生成的plist文件(就放在那个Library/Preferences 下)容易被拿到,而且还要自己做加密。
特性:1.当app删除了,又再次重新安装,这个保险柜里的信息还存在哦。 所以当你的某女同学登了APP并保存了密码,你重装了APP,如果不删除记录,你女票还是可以发现的。
2.安全!作为小白的我并不知道它实际上是存在哪里的。
2.Keychain 组成?
1.组成部分由 {N个标签(属性) + 一个重要数据} 组成!
2.结构可以看成是一个字典的形式大概是这样的: @{@"属性key1":@"属性值1",@"属性keyN":@"属性值N",@"valueData":@数据}
3.内容说明:
一个重要数据:就是密码password!
N个标签:也是属性,都是用来表明这条钥匙的,如我们的serviceName ,account 都是属性,他们对应的键为 kSecAttrAccount 和 kSecAttrAccount,还有系统给我们加的创建时间,修改时间等还有label,type,port,你自己打开文件进去看看吧,这些标签的任务就是来表明这条钥匙是独一无二的。
3.原始API操作
先来看看几个API
添加钥匙: OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
查询密码与查询标签: OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
更新钥匙信息: OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
删除钥匙: OSStatus SecItemDelete(CFDictionaryRef query)
先说明一下 这些API的关键在于1.是理解和配置好这个操作字典 2.注意返回的OSStatus 状态 3.CF对象与OC 之间的bridge
1.先来一发查找
过程:
1.(关键)先配置一个操作字典内容有:
kSecAttrService(属性),kSecAttrAccount(属性) 这些属性or标签是查找的依据
kSecReturnData(值为@YES 表明返回类型为data),kSecClass(值为kSecClassGenericPassword 表示重要数据为“一般密码”类型) 这些限制条件是返回结果类型的依据
2.然后用查找的API 得到查找状态和返回数据(密码)
3.最后如果状态成功那么将数据(密码)转换成string 返回
//用原生的API 实现查询密码
- (NSString *)passwordForService:(nonnull NSString *)service account:(nonnull NSString *)account{
//生成一个查询用的 可变字典
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:4];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; //表明为一般密码可能是证书或者其他东西
[dict setObject:service forKey:(__bridge id)kSecAttrService]; //输入service
[dict setObject:account forKey:(__bridge id)kSecAttrAccount]; //输入account
[dict setObject:@YES forKey:(__bridge id)kSecReturnData]; //返回Data
//查询
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)dict,&result);//核心API 查找是否匹配 和返回密码!
if (status != errSecSuccess) { //判断状态
return nil;
}
//返回数据
NSString *password = [[NSString alloc] initWithData:(__bridge_transfer NSData *)result encoding:NSUTF8StringEncoding];//转换成string
return password;
}
说明:其实关键就在于这个操作字典的配置上!
2.添加&更新
说明:当添加的时候我们一般需要判断一下当前钥匙串里面是否已经存在我们要添加的钥匙。如果已经存在我们就更新好了,不存在再添加,所以这两个操作一般写成一个函数搞定吧。
过程关键:1.检查是否已经存在 构建的查询用的操作字典:kSecAttrService,kSecAttrAccount,kSecClass(标明存储的数据是什么类型,值为kSecClassGenericPassword 就代表一般的密码)
2.添加用的操作字典: kSecAttrService,kSecAttrAccount,kSecClass,kSecValueData
3.更新用的操作字典1(用于定位需要更改的钥匙):kSecAttrService,kSecAttrAccount,kSecClass
操作字典2(新信息)kSecAttrService,kSecAttrAccount,kSecClass ,kSecValueData
//用原生的API 添加一条钥匙
-(BOOL)addItemWithService:(NSString *)service account:(NSString *)account password:(NSString *)password{
//先查查是否已经存在
//构造一个操作字典用于查询
NSMutableDictionary *searchDict = [[NSMutableDictionary alloc]initWithCapacity:4];
[searchDict setObject:service forKey:(__bridge id)kSecAttrService]; //标签service
[searchDict setObject:account forKey:(__bridge id)kSecAttrAccount]; //标签account
[searchDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存储的是一个密码
OSStatus status = -1;
CFTypeRef result =NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchDict, &result);
if (status == errSecItemNotFound) { //没有找到则添加
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 转换为 NSData
[searchDict setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密码
status = SecItemAdd((__bridge CFDictionaryRef)searchDict, NULL); //!!!!!关键的添加API
}else if (status == errSecSuccess){ //成功找到,说明钥匙已经存在则进行更新
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 转换为 NSData
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:searchDict];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密码
status = SecItemUpdate((__bridge CFDictionaryRef)searchDict, (__bridge CFDictionaryRef)dict);//!!!!关键的更新API
}
return (status == errSecSuccess);
}
其他操作的重要都在操作字典上-_-! 有特殊的东东遇到了再写上!
标签:account,Keychain,String,RSA,密码,NSString,let,Swift 来源: https://www.cnblogs.com/sundaysme/p/10361555.html