苹果登录,校验identityToken方式
概述
苹果授权登录之后返回用户ID,authorizationCode及identityToken,其中:
- authorizationCode 对应一种同苹果服务器交互的验证方式,国内三方登录,例如微信登录之类的都是这种验证方式,不过本文不涉及。
- identityToken 通过苹果服务器获取到的公钥进行token解密验证的方式,看起来密钥是轮动的,也很巧妙的实现了验证token是有效的,同时能够通过对比信息来确保信息的正确性
参考
https://blog.csdn.net/we_are_the_world_123/article/details/114943076
https://xyccstudio.cn/blogs/xblog/ios/login.html
验证流程
获取苹果的公钥
地址如下:点击查看
https://appleid.apple.com/auth/keys
获取到的内容如下:
{
"keys": [
{
"kty": "RSA",
"kid": "fh6Bs8C",
"use": "sig",
"alg": "RS256",
"n": "u704gotMSZc6CSSVNCZ1d0S9dZKwO2BVzfdTKYz8wSNm7R_KIufOQf3ru7Pph1FjW6gQ8zgvhnv4IebkGWsZJlodduTC7c0sRb5PZpEyM6PtO8FPHowaracJJsK1f6_rSLstLdWbSDXeSq7vBvDu3Q31RaoV_0YlEzQwPsbCvD45oVy5Vo5oBePUm4cqi6T3cZ-10gr9QJCVwvx7KiQsttp0kUkHM94PlxbG_HAWlEZjvAlxfEDc-_xZQwC6fVjfazs3j1b2DZWsGmBRdx1snO75nM7hpyRRQB4jVejW9TuZDtPtsNadXTr9I5NjxPdIYMORj9XKEh44Z73yfv0gtw",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "YuyXoY",
"use": "sig",
"alg": "RS256",
"n": "1JiU4l3YCeT4o0gVmxGTEK1IXR-Ghdg5Bzka12tzmtdCxU00ChH66aV-4HRBjF1t95IsaeHeDFRgmF0lJbTDTqa6_VZo2hc0zTiUAsGLacN6slePvDcR1IMucQGtPP5tGhIbU-HKabsKOFdD4VQ5PCXifjpN9R-1qOR571BxCAl4u1kUUIePAAJcBcqGRFSI_I1j_jbN3gflK_8ZNmgnPrXA0kZXzj1I7ZHgekGbZoxmDrzYm2zmja1MsE5A_JX7itBYnlR41LOtvLRCNtw7K3EFlbfB6hkPL-Swk5XNGbWZdTROmaTNzJhV-lWT0gGm6V1qWAK2qOZoIDa_3Ud0Gw",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "W6WcOKB",
"use": "sig",
"alg": "RS256",
"n": "2Zc5d0-zkZ5AKmtYTvxHc3vRc41YfbklflxG9SWsg5qXUxvfgpktGAcxXLFAd9Uglzow9ezvmTGce5d3DhAYKwHAEPT9hbaMDj7DfmEwuNO8UahfnBkBXsCoUaL3QITF5_DAPsZroTqs7tkQQZ7qPkQXCSu2aosgOJmaoKQgwcOdjD0D49ne2B_dkxBcNCcJT9pTSWJ8NfGycjWAQsvC8CGstH8oKwhC5raDcc2IGXMOQC7Qr75d6J5Q24CePHj_JD7zjbwYy9KNH8wyr829eO_G4OEUW50FAN6HKtvjhJIguMl_1BLZ93z2KJyxExiNTZBUBQbbgCNBfzTv7JrxMw",
"e": "AQAB"
}
]
}
解密token
分析token
一个identityToken的例子:
eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnRqZ2QuZGV2ZWxvcC5tb2JpbGV6Lldpc2VUViIsImV4cCI6MTY5MTY0Mzc3MSwiaWF0IjoxNjkxNTU3MzcxLCJzdWIiOiIwMDA0NDAuM2E5M2Y3MDY1NTE0NGY5Mjg5ZGJlY2Q2ODhhMmQxNzguMDc0NSIsImNfaGFzaCI6IjkyOEVmVEliSkVVUDBVQlhNRF9wZHciLCJlbWFpbCI6InRqZ2RpcHR2QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY5MTU1NzM3MSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.gt1DjUwX5oAzmYyoVoFtqjWv8D3SXkBm327RnadilOQxYbLLM82l45EWozjykRMhMtH4bWsP47m6DtBvwfA13kQg9HNTFCYOdm7j2SnF_3S5m_6sHvaxkK0VDbuk3Z-aWOcMlHMj4kFOdM9EMC5-1Qr_k9y6i7RRmh_xkAoapoFwSlLCb41J9qnornTbZnRT3CMgMngY90uxvlowYj38YpFW5Q1YDEQVtpDTz55yVIN7yPZ5bUlOHgj2wFWp_2IVo2qKCVgC4-qtoxsRDlR_Wghmv5lh_uwMGAkwBcsTS0eP0fGuYPNoZYuYzpiaLV9j3v01N191xgfUzKVa584Zeg
jwt token通过.把token分为三部分,第一部分 header 记录了加密方式,第二部分 body 是token记录的信息,第三部分是签名。
此token的三部分如下:
header
{
"kid": "fh6Bs8C",
"alg": "RS256"
}
body
{
"iss": "https://appleid.apple.com",
"aud": "com.xxxxxx",
"exp": 1691643771,
"iat": 1691557371,
"sub": "000440.3a93f70655144f9289dbecd688a2d178.0745",
"c_hash": "928EfTIbJEUP0UBXMD_pdw",
"email": "xxxxxx",
"email_verified": "true",
"auth_time": 1691557371,
"nonce_supported": true
}
签名
gt1DjUwX5oAzmYyoVoFtqjWv8D3SXkBm327RnadilOQxYbLLM82l45EWozjykRMhMtH4bWsP47m6DtBvwfA13kQg9HNTFCYOdm7j2SnF_3S5m_6sHvaxkK0VDbuk3Z-aWOcMlHMj4kFOdM9EMC5-1Qr_k9y6i7RRmh_xkAoapoFwSlLCb41J9qnornTbZnRT3CMgMngY90uxvlowYj38YpFW5Q1YDEQVtpDTz55yVIN7yPZ5bUlOHgj2wFWp_2IVo2qKCVgC4-qtoxsRDlR_Wghmv5lh_uwMGAkwBcsTS0eP0fGuYPNoZYuYzpiaLV9j3v01N191xgfUzKVa584Zeg
验证token
通过header解析出来的kid来选择一个密钥并且使用密钥进行token验证。
校验载荷
token没什么问题之后再校验一下上述body里边的内容同apple相关内容是否一致。即完成整体流程的验证。
整体流程代码
package login
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"io"
"math/big"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
)
const (
PUBLIC_KEY_REQ_URL = "https://appleid.apple.com/auth/keys"
APPLE_URL = "https://appleid.apple.com"
APPLICATION_CLIENT_ID = "xxx"
)
type JwtClaims struct {
jwt.StandardClaims
}
type JwtHeader struct {
Kid string `json:"kid"`
Alg string `json:"alg"`
}
type JwtKeys struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
Alg string `json:"alg"`
N string `json:"n"`
E string `json:"e"`
}
func VerifyIdentityToken(cliToken string, cliUserID string) error {
cliTokenArr := strings.Split(cliToken, ".")
if len(cliTokenArr) < 3 {
return errors.New("cliToken Split err")
}
cliHeader, err := jwt.DecodeSegment(cliTokenArr[0])
if err != nil {
return err
}
var jHeader JwtHeader
err = json.Unmarshal(cliHeader, &jHeader)
if err != nil {
return err
}
token, err := jwt.ParseWithClaims(cliToken, &JwtClaims{
}, func(token *jwt.Token) (interface{
}, error) {
pk := GetRSAPublicKey(jHeader.Kid)
return pk, nil
})
if err != nil {
return err
}
if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid {
if claims.Issuer != APPLE_URL || claims.Audience != APPLICATION_CLIENT_ID || claims.Subject != cliUserID {
return errors.New("verify token info fail, info is not match")
}
} else {
return errors.New("token claims parse fail")
}
return nil
}
func GetRSAPublicKey(kid string) *rsa.PublicKey {
var body []byte
resp, err := http.Get(PUBLIC_KEY_REQ_URL)
if err != nil {
return nil
} else {
defer resp.Body.Close()
_body, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
body = _body
}
var jKeys map[string][]JwtKeys
err = json.Unmarshal(body, &jKeys)
if err != nil {
return nil
}
var pubKey rsa.PublicKey
for _, data := range jKeys {
for _, val := range data {
if val.Kid == kid {
n_bin, _ := base64.RawURLEncoding.DecodeString(val.N)
n_data := new(big.Int).SetBytes(n_bin)
e_bin, _ := base64.RawURLEncoding.DecodeString(val.E)
e_data := new(big.Int).SetBytes(e_bin)
pubKey.N = n_data
pubKey.E = int(e_data.Uint64())
break
}
}
}
if pubKey.E <= 0 {
return nil
}
return &pubKey
}
使用事例
package main
import (
"fmt"
"xyccstudio/login"
)
func main() {
err := login.VerifyIdentityToken("eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnRqZ2QuZGV2ZWxvcC5tb2JpbGV6Lldpc2VUViIsImV4cCI6MTY5MTY0Mzc3MSwiaWF0IjoxNjkxNTU3MzcxLCJzdWIiOiIwMDA0NDAuM2E5M2Y3MDY1NTE0NGY5Mjg5ZGJlY2Q2ODhhMmQxNzguMDc0NSIsImNfaGFzaCI6IjkyOEVmVEliSkVVUDBVQlhNRF9wZHciLCJlbWFpbCI6InRqZ2RpcHR2QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY5MTU1NzM3MSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.gt1DjUwX5oAzmYyoVoFtqjWv8D3SXkBm327RnadilOQxYbLLM82l45EWozjykRMhMtH4bWsP47m6DtBvwfA13kQg9HNTFCYOdm7j2SnF_3S5m_6sHvaxkK0VDbuk3Z-aWOcMlHMj4kFOdM9EMC5-1Qr_k9y6i7RRmh_xkAoapoFwSlLCb41J9qnornTbZnRT3CMgMngY90uxvlowYj38YpFW5Q1YDEQVtpDTz55yVIN7yPZ5bUlOHgj2wFWp_2IVo2qKCVgC4-qtoxsRDlR_Wghmv5lh_uwMGAkwBcsTS0eP0fGuYPNoZYuYzpiaLV9j3v01N191xgfUzKVa584Zeg", "000440.3a93f70655144f9289dbecd688a2d178.0745")
fmt.Printf("check result %+v", err)
}