Sign in With Apple之服務(wù)端驗(yàn)證

介紹

2019年之后涡贱,對(duì)于Apple App來說趴酣,如果要支持第三方登錄膘融,則必須同時(shí)支持蘋果的第三方登錄,即Sign in With Apple, 本文主要介紹如何使用Go語言實(shí)現(xiàn)Sign in With Apple時(shí)服務(wù)端的驗(yàn)證, 即Generate and Validate Tokens狼渊。或者不支持第三方登錄, 直接使用電話號(hào)碼或者賬號(hào)密碼的方式進(jìn)行注冊(cè)以及登錄类垦。

登錄流程

流程大概可以描述為:

  1. app請(qǐng)求通過Apple進(jìn)行第三方登錄狈邑,此時(shí),客戶端將會(huì)獲得包括用戶唯一憑證UserID(與微信的OpenId類似), 用戶全名Full Name, 驗(yàn)證用的Code(IdentityCode)以及驗(yàn)證用的Token(IdentityToken)蚤认。

  2. 客戶端將獲得的數(shù)據(jù)發(fā)送給服務(wù)器米苹,由服務(wù)器通過IdentityCode或者IdentityToken來驗(yàn)證此次登錄是否有效。

  3. 如果驗(yàn)證通過, 服務(wù)端處理完自己內(nèi)部的登錄流程后, 將對(duì)應(yīng)的登錄結(jié)果(狀態(tài))返回給客戶端砰琢。

在第二步服務(wù)器的驗(yàn)證過程中蘸嘶,服務(wù)器只需要選擇Code或者Token中的任意一種進(jìn)行驗(yàn)證即可:

  1. IdentityToken: 根據(jù)Apple官方文檔, Token驗(yàn)證方式為JSON WEB Token(JWT), 按照對(duì)應(yīng)的方式進(jìn)行驗(yàn)證即可。
  2. IdentityCode: 根據(jù)Apple官方文檔, 通過Code驗(yàn)證需要Apple開發(fā)者對(duì)該App進(jìn)行配置的額外client_id, client_secret以及redirect_uri三個(gè)參數(shù)陪汽。

IdentityToken驗(yàn)證

此種驗(yàn)證方法為傳統(tǒng)的JWT驗(yàn)證, Token由Header, Payload以及Signature三部分組成, 通過JSON序列化每一部分训唱,然后使用Base64URL編碼后通過.拼接起來的字符串。

  1. Header: 包括的字段如下,

    • kid: 表示用于驗(yàn)證簽名的Apple公鑰
    • alg: 表示用于簽名的算法
  2. Payload: 包括的字段有如下,

    • iss(string): 表示Token簽發(fā)機(jī)構(gòu), 值固定為: https://appleid.apple.com
    • aud(string): 表示Apple App的ID
    • exp(int64): 表示Token的過期時(shí)間, 時(shí)間戳
    • iat(int64): 表示client_secret生成時(shí)間掩缓,時(shí)間戳
    • sub(string): 表示用戶唯一標(biāo)識(shí)
    • c_hash(string): 文檔中沒看到這個(gè)字段, 作用未知
    • auth_time(int64): 表示簽名生成時(shí)間
    • email(string): 表示用戶郵箱, 可能是真實(shí)的也可能Apple處理過的密文郵件地址雪情,取決于用戶登錄時(shí)是否選擇了隱藏郵箱
    • email_verified(bool): 表示用戶郵箱是否已驗(yàn)證, 由于Apple總是返回已驗(yàn)證了的郵箱, 所以這個(gè)字段的值總是為true, 但是需要注意的是, Apple返回的true, 可能是字符串也可能是bool類型, 需要自己處理一下。
    • nonce(string): 只有當(dāng)發(fā)起登錄請(qǐng)求的時(shí)候傳遞了此參數(shù), 在驗(yàn)證的時(shí)候才會(huì)返回你辣,目的是為了降低被攻擊的可能性
    • nonce_supported(bool): 表示是否支持nonce, 如果為true, 則需要判斷nonce字段值是否正確
    • is_private_email(bool): 表示用戶提供的郵箱地址是否是Apple處理了的代理郵箱地址
    • real_user_status(int): 表示用戶是否是真實(shí)用戶: 0(Unsupported: 表示當(dāng)前系統(tǒng)版本不支持該字段的值, 只有在IOS 14及以上版本, macOS 11及以上版本, watchOS 7及以上版本才支持), 1(Unknown: 系統(tǒng)無法識(shí)別是否是真實(shí)用戶), 2(LikelyReal: 幾乎可以確定為真實(shí)用戶)
  3. Signature: 表示簽名字段巡通,用Base64URL對(duì)Header和Payload分別編碼,然后用.拼接, 最后使用RSA以及SHA256進(jìn)行簽名得到的結(jié)果

一個(gè)Header和Payload的例子為:

{
    "alg": "RS256",
    "kid": "ABC123DEFG"
}
{
    "iss": "DEF123GHIJ",
    "iat": 1437179036,
    "exp": 1493298100,
    "aud": "https://appleid.apple.com",
    "sub": "com.mytest.app"
}

一個(gè)IdentityToken例子如下:

eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmZ1bi5BcHBsZUxvZ2luIiwiZXhwIjoxNTY4NzIxNzY5LCJpYXQiOjE1Njg3MjExNjksInN1YiI6IjAwMDU4MC4wODdjNTU0ZGNlMzU0NjZmYTg1YzVhNWQ1OTRkNTI4YS4wODAxIiwiY19oYXNoIjoiel9KY0RscFczQjJwN3ExR0Nna1JaUSIsImF1dGhfdGltZSI6MTU2ODcyMTE2OX0.WmSa4LzOzYsdwTqAJ_8mub4Ls3eyFkxZoGLoy-U7DatsTd_JEwAs3_OtV4ucmj6ENT3153iCpYY6vBxSQromOMcXsN74IrUQew24y_zflN2g4yU8ZVvBCbTrR_6p9f2fbeWjZiyNcbPCha0dv45E3vBjyHhmffWnk3vyndBBiwwuqod4pyCZ3UECf6Vu-o7dygKFpMHPS1ma60fEswY5d-_TJAFk1HaiOfFo0XbL6kwqAGvx8HnraIxyd0n8SbBVxV_KDxf15hdotUizJDW7N2XMdOGQpNFJim9SrEeBhn9741LWqkWCgkobcvYBZsrvnUW6jZ87SLi15rvIpq8_fw

根據(jù)上面可以得出驗(yàn)證IdentityToken的步驟為:

  1. .為分隔點(diǎn), 將IdentityToken分隔為三部分, 第三部分為簽名, 留著用于驗(yàn)證

  2. 使用Base64URL解碼對(duì)應(yīng)的Header和Payload, 并JSON反序列化為對(duì)應(yīng)的結(jié)構(gòu)體(或者鍵值對(duì)), 并且對(duì)Payload中相應(yīng)對(duì)值進(jìn)行驗(yàn)證舍哄,如exp, sub, iat, aud

  3. 通過接口從Apple Server獲取RSA公鑰宴凉,接口地址https://appleid.apple.com/auth/keys, 這里需要注意, 獲取到的結(jié)果通常為兩個(gè),需要用選擇與Header中的kid值匹配的那個(gè)Key

  4. 步驟3返回的Key中包含了RSA公鑰中的NE的值表悬,同樣是用Base64URL編碼后的值, 需要解碼, 然后再構(gòu)造RSA公鑰

  5. 得到公鑰后弥锄,將步驟1中得到的Base64URL編碼的Header和Payload再次拼接起來,然后調(diào)用rsa.VerifyPKCS1v15()方法進(jìn)行簽名驗(yàn)證, 注意這里的Hash類型為SHA256

驗(yàn)證代碼如下:

func (v *Validator) CheckIdentityToken(token string) (JWTToken, error) {
    if token == "" {
        return nil, ErrInvalidIdentityToken
    }
    appleToken, err := parseToken(token)
    if err != nil {
        return nil, err
    }
    key, err := fetchKeysFromApple(appleToken.header.Kid)
    if err != nil {
        return nil, err
    }
    if key == nil {
        return nil, ErrFetchKeysFail
    }
    
    pubKey, err := generatePubKey(key.N, key.E)
    if err != nil {
        return nil, err
    }
    
    //利用獲取到的公鑰解密token中的簽名數(shù)據(jù)
    sig, err := decodeSegment(appleToken.sign)
    if err != nil {
        return nil, err
    }
    
    //蘋果使用的是SHA256
    var h hash.Hash
    switch appleToken.header.Alg {
    case "RS256":
        h = crypto.SHA256.New()
    case "RS384":
        h = crypto.SHA384.New()
    case "RS512":
        h = crypto.SHA512.New()
    }
    if h == nil {
        return nil, ErrInvalidHashType
    }
    
    h.Write([]byte(appleToken.headerStr + "." + appleToken.claimsStr))
    
    return appleToken, rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, h.Sum(nil), sig)
}

IdentityCode驗(yàn)證

按照官方文檔, IdentityCode的驗(yàn)證相對(duì)來說限制要高一點(diǎn)蟆沫,沒有那么通用, 因?yàn)轵?yàn)證過程中需要用到client_id, client_secret, redirect_uri三個(gè)參數(shù), 由于每個(gè)Apple App這三個(gè)參數(shù)都不相同, 所以沒有IdentityToken那么通用籽暇。

根據(jù)官方文檔, IdentityCode的驗(yàn)證需要調(diào)用接口向Apple Server驗(yàn)證, 接口地址為: https://appleid.apple.com/auth/token

文檔中已經(jīng)說得很明白, 具體代碼如下:

func (v *Validator) CheckIdentityCode(code string) (*TokenResponse, error) {
    if code == "" {
        return nil, ErrInvalidIdentityCode
    }
    if v.clientID == "" {
        return nil, ErrInvalidClientID
    }
    if v.clientSecret == "" {
        return nil, ErrInvalidClientSecret
    }
    //驗(yàn)證IdentityCode時(shí)需要填寫redirect_uri參數(shù),且redirect_uri參數(shù)必須是https協(xié)議
    if uri := strings.ToLower(v.redirectUri); strings.HasPrefix(uri, "https://") {
        return nil, ErrInvalidRedirectURI
    }
    
    param := fmt.Sprintf("client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s", v.clientID, v.clientSecret, code, v.redirectUri)
    rder := strings.NewReader(param)
    response, err := http.Post("https://appleid.apple.com/auth/token", "application/x-www-form-urlencoded", rder)
    if err != nil {
        return nil, err
    }
    defer response.Body.Close()
    
    if response.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("checking identityCode from apple server fail: %d", response.StatusCode)
    }
    
    data, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, err
    }
    
    var tkResult *TokenResponse
    if err = json.Unmarshal(data, &tkResult); err != nil {
        return nil, err
    }
    return tkResult, nil
}

詳細(xì)代碼請(qǐng)前往Github
原文地址Golang實(shí)現(xiàn)Sign in With Apple服務(wù)端登錄驗(yàn)證

參考資料

Sign in With Apple

jwt-go

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饭庞,一起剝皮案震驚了整個(gè)濱河市戒悠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舟山,老刑警劉巖绸狐,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卤恳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡寒矿,警方通過查閱死者的電腦和手機(jī)突琳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來符相,“玉大人拆融,你說我怎么就攤上這事≈魑。” “怎么了冠息?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)孕索。 經(jīng)常有香客問我,道長(zhǎng)躏碳,這世上最難降的妖魔是什么搞旭? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮菇绵,結(jié)果婚禮上肄渗,老公的妹妹穿的比我還像新娘。我一直安慰自己咬最,他們只是感情好翎嫡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著永乌,像睡著了一般惑申。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翅雏,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天圈驼,我揣著相機(jī)與錄音,去河邊找鬼望几。 笑死绩脆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的橄抹。 我是一名探鬼主播靴迫,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼楼誓!你這毒婦竟也來了玉锌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤慌随,失蹤者是張志新(化名)和其女友劉穎芬沉,沒想到半個(gè)月后躺同,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丸逸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蹋艺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黄刚。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捎谨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出憔维,到底是詐尸還是另有隱情涛救,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布业扒,位于F島的核電站检吆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏程储。R本人自食惡果不足惜蹭沛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望章鲤。 院中可真熱鬧摊灭,春花似錦、人聲如沸败徊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)皱蹦。三九已至煤杀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間根欧,已是汗流浹背怜珍。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凤粗,地道東北人酥泛。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嫌拣,于是被迫代替她去往敵國(guó)和親柔袁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348