Keychain Services -- kSecClassGenericPassword

今天做一個需求束铭,要求獲取到設(shè)備的IDFA,但是我們都知道蚌堵,這個值是會變的买决,會受到用戶的影響,所以就想看能不能做一個持久化(對于用戶不允許的情況吼畏,可以自己生成一個督赤,實現(xiàn)有具體的開源代碼),隨著軟件的卸載和再安裝泻蚊,這個值始終是不變的躲舌。
就想起來了keychain--這個系統(tǒng)級的存儲。這也就是為什么有些軟件在卸載后重新安裝后用戶名和密碼還在的方法之一(因為還有其他方法)性雄。

除了做數(shù)據(jù)存儲没卸,其實它還可以做APP間的數(shù)據(jù)共享。

因為這部分概念的東西并不多秒旋,所以下面將直接上代碼约计,遇到問題順帶著再說;另:mac和iOS不同迁筛,這里只說iOS的煤蚌。

所有的詳細資料在這里

一细卧、Keychain

1尉桩、概念

Keychain是一個儲存在文件下的簡單數(shù)據(jù)庫。通常情況下贪庙,app里有一個簡單的keychain蜘犁,可以被所有的app使用。
Keychain有任意數(shù)量的鑰匙鏈(item)插勤,該鑰匙鏈里包含一組屬性沽瘦。該屬性和鑰匙鏈的類型相關(guān)革骨。創(chuàng)建日期和label對所有的鑰匙鏈?zhǔn)峭ㄓ玫摹F渌亩际歉鶕?jù)鑰匙鏈的類型不同而不同析恋,比如良哲,generic password類型包含service和account屬性。
鑰匙鏈可以使用kSecAttrSynchronizable同步屬性助隧,被標(biāo)記為該屬性的值都可以被放置在iCloud的鑰匙鏈中筑凫,它會被自動同步到相同賬號的設(shè)備上。
有些鑰匙鏈需要保護起來并村,比如密碼和私人key巍实,都會被加密;對于那些不需要被保護的鑰匙鏈哩牍,比如證書棚潦,就不會被加密。
在iOS設(shè)備上(手機)膝昆,當(dāng)屏幕被解鎖時丸边,鑰匙鏈的訪問權(quán)限就會被打開。

2荚孵、訪問

首先說明其能保存的類型妹窖,有5種:
  • kSecClassGenericPassword:存儲一般密碼,比較常用這個
  • kSecClassInternetPassword:存儲網(wǎng)絡(luò)密碼
  • kSecClassCertificate:存儲證書
  • kSecClassKey:存儲私有密鑰
  • kSecClassIdentity:存儲一個包含證書和私有密鑰的item
(1)添加
  • 方法:OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
    ??attributes有四部分組成:
    ??a收叶、item的類型骄呼,必選,key=kSecClass,value就是上面說的5種;
    ??b判没、item的存儲數(shù)據(jù)蜓萄;
    ??c、屬性哆致,可以用來做一些標(biāo)記用于查找或者程序之間的數(shù)據(jù)分享绕德,但是不同的kSecClass有不同的屬性;
    ??d摊阀、返回數(shù)據(jù)類型耻蛇,它的設(shè)置影響參數(shù)result。
    ??result:對添加的item的引用胞此,如果沒有什么需要臣咖,可以設(shè)為nil
  • 使用:
- (void)saveBasicInfo {
    CFMutableDictionaryRef mutableDicRef = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(mutableDicRef, kSecClass, kSecClassGenericPassword);  //類型
    CFDateRef dateRef = CFDateCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent());
    CFDictionarySetValue(mutableDicRef, kSecAttrCreationDate, dateRef);  //創(chuàng)建時間
    CFStringRef strRef = CFSTR("save generic password 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrDescription, strRef);  //描述
    CFStringRef commentStrRef = CFSTR("generic password comment 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrComment, commentStrRef); //備注、注釋
    CFNumberRef creatorRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "zhou");
    CFDictionarySetValue(mutableDicRef, kSecAttrCreator, creatorRef);  //創(chuàng)建者漱牵,只能是四個字符長度
    CFNumberRef typeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "type");
    CFDictionarySetValue(mutableDicRef, kSecAttrType, typeRef);   // 類型
    CFStringRef labelRef = CFSTR("label 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrLabel, labelRef); //標(biāo)簽夺蛇,用戶可以看到
    CFDictionarySetValue(mutableDicRef, kSecAttrIsInvisible, kCFBooleanTrue);  //是否不可見、是否隱藏(kCFBooleanTrue酣胀、kCFBooleanFalse)
    CFDictionarySetValue(mutableDicRef, kSecAttrIsNegative, kCFBooleanFalse); //標(biāo)記是否有密碼(kCFBooleanTrue刁赦、kCFBooleanFalse)
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrAccount, accountRef);  //賬戶娶聘,相同的賬戶不允許儲存兩次,否則會報錯
    CFStringRef serviceRef = CFSTR("service");
    CFDictionarySetValue(mutableDicRef, kSecAttrService, serviceRef);  //所具有的服務(wù)
    CFMutableDataRef genericRef = CFDataCreateMutable(kCFAllocatorDefault, 0);
    char * generic_char = "personal generic 2";
    CFDataAppendBytes(genericRef, (const UInt8 *)generic_char, sizeof("personal generic 2"));
    CFDictionarySetValue(mutableDicRef, kSecAttrGeneric, genericRef);  //用戶自定義
    CFDictionarySetValue(mutableDicRef, kSecValueData, genericRef);  //保存值
    OSStatus status = SecItemAdd(mutableDicRef, nil);  //相同的東西只能添加一次甚脉,不能重復(fù)添加丸升,重復(fù)添加會報錯
    if (status == errSecSuccess) {
        NSLog(@"success");
    } else {
        NSLog(@"%@",@(status));
    }
... //神略的是前面創(chuàng)建的變量的釋放
}
  • 總結(jié):
    ??1、要保證每次添加都是不同賬戶
    ??2牺氨、注意變量內(nèi)存釋放
    ??3狡耻、要注意數(shù)據(jù)類型要對
(2)查找(最好的辦法是根據(jù)用戶名查找)
  • 方法:OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
    ??query的組成:
    ??a、item的類型猴凹,必選夷狰,key=kSecClass,value就是上面說的5種;
    ??b、要查找的屬性以及值郊霎,這個可以用來篩選掉多余的選項沼头,這個值越詳細,最后獲取的結(jié)果越精確
    ??c歹篓、做進一步的限制瘫证,比如大小寫是否敏感等等
    ??d、返回匹配項的個數(shù)庄撮,但是注意kSecReturnData
    和kSecMatchLimit不能共存,更多的kSecMatchLimit可以搭配kSecReturnAttributes使用
  • 使用:
- (OSStatus)keychainMatching {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    //查找類型
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    //匹配的屬性 越詳細毙籽,越精準(zhǔn)
//    CFStringRef strRef = CFSTR("save generic password");
//    CFDictionarySetValue(queryDic, kSecAttrDescription, strRef);  //描述
//    CFStringRef commentStrRef = CFSTR("generic password comment");
//    CFDictionarySetValue(queryDic, kSecAttrComment, commentStrRef); //備注洞斯、注釋
//    CFNumberRef creatorRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "zhou");
//    CFDictionarySetValue(queryDic, kSecAttrCreator, creatorRef);  //創(chuàng)建者,只能是四個字符長度
//    CFNumberRef typeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "type");
//    CFDictionarySetValue(queryDic, kSecAttrType, typeRef);   // 類型
//    CFStringRef labelRef = CFSTR("label");
//    CFDictionarySetValue(queryDic, kSecAttrLabel, labelRef); //標(biāo)簽坑赡,用戶可以看到
    //查找的參數(shù)
//    CFDictionarySetValue(queryDic, kSecMatchLimit, kSecMatchLimitAll);  //可以控制當(dāng)key=kSecReturnAttributes時返回值的個數(shù)
    //返回類型
//    CFDictionarySetValue(queryDic, kSecReturnData, kCFBooleanTrue);
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching(queryDic, &result);
    if (status == errSecSuccess) {
        NSLog(@"success:%@",result);
        if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //類型判斷
            NSLog(@"Dictionary");
        }
    } else {
        NSLog(@"%@",@(status));
    }
...//神略的是前面創(chuàng)建的變量的釋放
}
  • 總結(jié):
    這里主要注意一下返回類型就好了@尤纭!毅否!
(3)刪除
  • 方法:OSStatus SecItemDelete(CFDictionaryRef query)
    query可以直接自己創(chuàng)建也可以是查找后獲得的亚铁,具體使用看下面的使用。
  • 使用:
方法一:
//直接刪除
- (void)deleteItemDirect {
   //刪除CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
   CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
   CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);  //這個不要丟C印E且纭!
   CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
   CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
   if (accountRef) {
       CFRelease(accountRef);
   }
   OSStatus delStatus = SecItemDelete(queryDic);
   if (delStatus == errSecSuccess) {
       NSLog(@"delete success");
   } else {
       NSLog(@"%@",@(delStatus));
   }
}
方法二:
//先查找捆探,再刪除
- (void)deleteItemWithQuery {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
    if (accountRef) {
        CFRelease(accountRef);
    }
    
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFDictionarySetValue(queryDic, kSecReturnRef, kCFBooleanTrue);  //這一句話必須要有然爆,否則刪除不了
//    CFDictionarySetValue(queryDic, kSecReturnPersistentRef, kCFBooleanTrue);  //不能用這句,這句是做什么用的呢黍图?
    CFTypeRef result = NULL;
    OSStatus queryStatus = SecItemCopyMatching(queryDic, &result);
    if (queryDic) {
        CFRelease(queryDic);
    }
    if (queryStatus != errSecSuccess) {
        NSLog(@"query failed");
        return;
    }
    if (CFGetTypeID(result) == CFDictionaryGetTypeID()) {
        OSStatus delStatus = SecItemDelete(result);
        if (delStatus == errSecSuccess) {
            NSLog(@"delete success");
        } else {
            NSLog(@"delete failed:%@",@(delStatus));
        }
    } else if (CFGetTypeID(result) == CFArrayGetTypeID()) {
        CFArrayRef arrRef = result;
        for (CFIndex i = 0; i < CFArrayGetCount(arrRef); i++) {
            OSStatus delStatus = SecItemDelete(CFArrayGetValueAtIndex(arrRef, i));
            if (delStatus == errSecSuccess) {
                NSLog(@"delete success");
            } else {
                NSLog(@"delete failed:%@",@(delStatus));
            }
        }
    }
}
  • 總結(jié):
    1曾雕、不要忘記設(shè)置item的類型
    2、如果是先查找然后再刪除助被,則需要加上kSecReturnRef或者kSecReturnPersistentRef(這個還沒搞懂)
(4)剖张、更新
  • 方法:OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
    attributesToUpdate:包含需要去更新的
    在使用上切诀,同樣有可以直接定義,然后去更新(其實其在內(nèi)部先查找了)搔弄;也可以先查找幅虑,再把查找到的作為參數(shù)去更新。
  • 使用:
方法一:
//更新
- (void)updateItem {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
    if (accountRef) {
        CFRelease(accountRef);
    }
    CFMutableDictionaryRef updateDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFStringRef strRef = CFSTR("save generic password update");
    CFDictionarySetValue(updateDic, kSecAttrDescription, strRef);  //描述
    OSStatus updateStatus = SecItemUpdate(queryDic, updateDic);
    if (updateStatus == errSecSuccess) {
        NSLog(@"success");
    } else {
        NSLog(@"update Failed:%@",@(updateStatus));
    }
    CFRelease(queryDic);
    CFRelease(updateDic);
}
方法二:
- (void)updateItemAfterSearch {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
    if (accountRef) {
        CFRelease(accountRef);
    }
    
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFDictionarySetValue(queryDic, kSecReturnRef, kCFBooleanTrue);  //這一句話必須要有肯污,否則刪除不了
    //    CFDictionarySetValue(queryDic, kSecReturnPersistentRef, kCFBooleanTrue);  //不能用這句翘单,這句是做什么用的呢?
    CFTypeRef result = NULL;
    OSStatus queryStatus = SecItemCopyMatching(queryDic, &result);
    if (queryDic) {
        CFRelease(queryDic);
    }
    if (queryStatus != errSecSuccess) {
        NSLog(@"query failed");
        return;
    }
    if (CFGetTypeID(result) != CFDictionaryGetTypeID()) {
        return;
    }
    CFMutableDictionaryRef updateDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFStringRef strRef = CFSTR("save generic password update with query");
    CFDictionarySetValue(updateDic, kSecAttrDescription, strRef);  //描述
    OSStatus updateStatus = SecItemUpdate(result, updateDic);
    if (updateStatus == errSecSuccess) {
        NSLog(@"success");
    } else {
        NSLog(@"update Failed:%@",@(updateStatus));
    }
    CFRelease(updateDic);
}
(5)獲取值
  • 返回屬性kSecReturnAttributes
    屬性的返回都是以集合形式返回蹦渣,即要么是數(shù)組+字典哄芜,或者就只有字典(默認),所以只要確定了返回類型柬唯,然后按照key讀取就可以了认臊。如下:
- (OSStatus)keychainMatching {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    //查找類型
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);  //賬戶
    //查找的參數(shù)
    CFDictionarySetValue(queryDic, kSecMatchLimit, kSecMatchLimitOne);  //可以控制當(dāng)key=kSecReturnAttributes時返回值的個數(shù)
    //返回類型
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching(queryDic, &result);
    if (status == errSecSuccess) {
        NSLog(@"success:%@",result);
        if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //類型判斷
            NSLog(@"label:%@",CFDictionaryGetValue(result, kSecAttrDescription));
        }
    } else {
        NSLog(@"%@",@(status));
    }
    if (queryDic) {
        CFRelease(queryDic);
    }
    return status;
}
  • 返回值為kSecReturnData
- (OSStatus)keychainMatching {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    //查找類型
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);  //賬戶
    //查找的參數(shù)
    //返回類型
    CFDictionarySetValue(queryDic, kSecReturnData, kCFBooleanTrue);
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching(queryDic, &result);
    if (status == errSecSuccess) {
        NSLog(@"success:%@",result);
        if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //類型判斷
            NSLog(@"label:%@",CFDictionaryGetValue(result, kSecAttrDescription));
        } else if (CFGetTypeID(result) == CFDataGetTypeID()) {
            const UInt8 * str = CFDataGetBytePtr(result);
            NSLog(@"%s",str);
        }
    } else {
        NSLog(@"%@",@(status));
    }
    if (queryDic) {
        CFRelease(queryDic);
    }
    return status;
}
  • 其他(kSecReturnRef和kSecReturnPersistentRef)
    這兩個我暫時還沒搞懂,搞懂了補上3荨J纭!

以上代碼在這里

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拘央,一起剝皮案震驚了整個濱河市涂屁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灰伟,老刑警劉巖拆又,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異栏账,居然都是意外死亡帖族,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門挡爵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竖般,“玉大人,你說我怎么就攤上這事茶鹃』恋瘢” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵前计,是天一觀的道長胞谭。 經(jīng)常有香客問我,道長男杈,這世上最難降的妖魔是什么丈屹? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上旺垒,老公的妹妹穿的比我還像新娘彩库。我一直安慰自己,他們只是感情好先蒋,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布骇钦。 她就那樣靜靜地躺著,像睡著了一般竞漾。 火紅的嫁衣襯著肌膚如雪眯搭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天业岁,我揣著相機與錄音鳞仙,去河邊找鬼。 笑死笔时,一個胖子當(dāng)著我的面吹牛棍好,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播允耿,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼借笙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了较锡?” 一聲冷哼從身側(cè)響起业稼,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚂蕴,沒想到半個月后盼忌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡掂墓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了看成。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片君编。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖川慌,靈堂內(nèi)的尸體忽然破棺而出吃嘿,到底是詐尸還是另有隱情,我是刑警寧澤梦重,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布兑燥,位于F島的核電站,受9級特大地震影響琴拧,放射性物質(zhì)發(fā)生泄漏降瞳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挣饥。 院中可真熱鬧除师,春花似錦、人聲如沸扔枫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽短荐。三九已至倚舀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忍宋,已是汗流浹背痕貌。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留讶踪,地道東北人芯侥。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像乳讥,于是被迫代替她去往敵國和親柱查。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容