介紹
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è)以及登錄类垦。
登錄流程
流程大概可以描述為:
app請(qǐng)求通過Apple進(jìn)行第三方登錄狈邑,此時(shí),客戶端將會(huì)獲得包括用戶唯一憑證UserID(與微信的OpenId類似), 用戶全名Full Name, 驗(yàn)證用的Code(IdentityCode)以及驗(yàn)證用的Token(IdentityToken)蚤认。
客戶端將獲得的數(shù)據(jù)發(fā)送給服務(wù)器米苹,由服務(wù)器通過IdentityCode或者IdentityToken來驗(yàn)證此次登錄是否有效。
如果驗(yàn)證通過, 服務(wù)端處理完自己內(nèi)部的登錄流程后, 將對(duì)應(yīng)的登錄結(jié)果(狀態(tài))返回給客戶端砰琢。
在第二步服務(wù)器的驗(yàn)證過程中蘸嘶,服務(wù)器只需要選擇Code或者Token中的任意一種進(jìn)行驗(yàn)證即可:
- IdentityToken: 根據(jù)Apple官方文檔, Token驗(yàn)證方式為JSON WEB Token(JWT), 按照對(duì)應(yīng)的方式進(jìn)行驗(yàn)證即可。
- 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編碼后通過.
拼接起來的字符串。
-
Header: 包括的字段如下,
- kid: 表示用于驗(yàn)證簽名的Apple公鑰
- alg: 表示用于簽名的算法
-
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í)用戶)
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的步驟為:
以
.
為分隔點(diǎn), 將IdentityToken分隔為三部分, 第三部分為簽名, 留著用于驗(yàn)證使用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
通過接口從Apple Server獲取RSA公鑰宴凉,接口地址https://appleid.apple.com/auth/keys, 這里需要注意, 獲取到的結(jié)果通常為兩個(gè),需要用選擇與Header中的
kid
值匹配的那個(gè)Key步驟3返回的Key中包含了RSA公鑰中的
N
和E
的值表悬,同樣是用Base64URL編碼后的值, 需要解碼, 然后再構(gòu)造RSA公鑰得到公鑰后弥锄,將步驟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)證