2019年之后,对于Apple App来说,如果要支持第三方登录,则必须同时支持苹果的第三方登录,即Sign in With Apple, 本文主要介绍如何使用Go语言实现Sign in With Apple时服务端的验证, 即Generate and Validate Tokens。或者不支持第三方登录, 直接使用电话号码或者账号密码的方式进行注册以及登录。
流程大概可以描述为:
app请求通过Apple进行第三方登录,此时,客户端将会获得包括用户唯一凭证UserID(与微信的OpenId类似), 用户全名Full Name, 验证用的Code(IdentityCode)以及验证用的Token(IdentityToken)。
客户端将获得的数据发送给服务器,由服务器通过IdentityCode或者IdentityToken来验证此次登录是否有效。
如果验证通过, 服务端处理完自己内部的登录流程后, 将对应的登录结果(状态)返回给客户端。
在第二步服务器的验证过程中,服务器只需要选择Code或者Token中的任意一种进行验证即可:
client_id
, client_secret
以及redirect_uri
三个参数。IdentityToken验证
此种验证方法为传统的JWT验证, Token由Header, Payload以及Signature三部分组成, 通过JSON序列化每一部分,然后使用Base64URL编码后通过.
拼接起来的字符串。
Header: 包括的字段如下,
Payload: 包括的字段有如下,
true
, 但是需要注意的是, Apple返回的true
, 可能是字符串也可能是bool类型, 需要自己处理一下。Signature: 表示签名字段,用Base64URL对Header和Payload分别编码,然后用.
拼接, 最后使用RSA以及SHA256进行签名得到的结果
一个Header和Payload的例子为:
{
"alg": "RS256",
"kid": "ABC123DEFG"
}
{
"iss": "DEF123GHIJ",
"iat": 1437179036,
"exp": 1493298100,
"aud": "https://appleid.apple.com",
"sub": "com.mytest.app"
}
一个IdentityToken例子如下:
eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmZ1bi5BcHBsZUxvZ2luIiwiZXhwIjoxNTY4NzIxNzY5LCJpYXQiOjE1Njg3MjExNjksInN1YiI6IjAwMDU4MC4wODdjNTU0ZGNlMzU0NjZmYTg1YzVhNWQ1OTRkNTI4YS4wODAxIiwiY19oYXNoIjoiel9KY0RscFczQjJwN3ExR0Nna1JaUSIsImF1dGhfdGltZSI6MTU2ODcyMTE2OX0.WmSa4LzOzYsdwTqAJ_8mub4Ls3eyFkxZoGLoy-U7DatsTd_JEwAs3_OtV4ucmj6ENT3153iCpYY6vBxSQromOMcXsN74IrUQew24y_zflN2g4yU8ZVvBCbTrR_6p9f2fbeWjZiyNcbPCha0dv45E3vBjyHhmffWnk3vyndBBiwwuqod4pyCZ3UECf6Vu-o7dygKFpMHPS1ma60fEswY5d-_TJAFk1HaiOfFo0XbL6kwqAGvx8HnraIxyd0n8SbBVxV_KDxf15hdotUizJDW7N2XMdOGQpNFJim9SrEeBhn9741LWqkWCgkobcvYBZsrvnUW6jZ87SLi15rvIpq8_fw
根据上面可以得出验证IdentityToken的步骤为:
以.
为分隔点, 将IdentityToken分隔为三部分, 第三部分为签名, 留着用于验证
使用Base64URL解码对应的Header和Payload, 并JSON反序列化为对应的结构体(或者键值对), 并且对Payload中相应对值进行验证,如exp, sub, iat, aud
通过接口从Apple Server获取RSA公钥,接口地址https://appleid.apple.com/auth/keys, 这里需要注意, 获取到的结果通常为两个,需要用选择与Header中的kid
值匹配的那个Key
步骤3返回的Key中包含了RSA公钥中的N
和E
的值,同样是用Base64URL编码后的值, 需要解码, 然后再构造RSA公钥
得到公钥后,将步骤1中得到的Base64URL编码的Header和Payload再次拼接起来,然后调用rsa.VerifyPKCS1v15()方法进行签名验证, 注意这里的Hash类型为SHA256
验证代码如下:
package goSignInWithApple
import (
"account-api/pkg/http"
"account-api/pkg/oauth2/errors"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"github.com/dgrijalva/jwt-go"
"io/ioutil"
"math/big"
"strings"
)
const (
GetApplePublicKeys = "https://appleid.apple.com/auth/keys"
AppleUrl = "https://appleid.apple.com"
ClientId = "com.xxx.xxx" //这个为app端ios的包名,问ios的同学就可以知道
)
type (
JwtClaims struct {
CHash string `json:"c_hash"`
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
AuthTime int `json:"auth_time"`
NonceSupported bool `json:"nonce_supported"`
jwt.StandardClaims
// jwt中clamis的基础字段,上面几个为苹果官方自定义的字段,很多人不知
// 道除基础字段以外的第三方自定义字段如何接受,只需要像上面一样在基础字段同
// 级定义就行
}
JwtHeader struct {
Kid string `json:"kid"`
Alg string `json:"alg"`
}
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"`
}
)
// VerifyIdentityToken 认证客户端传递过来的token是否有效
func VerifyIdentityToken(cliToken string, cliUserID string) (error, *JwtClaims) {
// 数据由 头部、载荷、签名 三部分组成
cliTokenArr := strings.Split(cliToken, ".")
if len(cliTokenArr) < 3 {
return errors.New("cliToken Split err"), nil
}
// 解析cliToken的header获取kid
cliHeader, err := jwt.DecodeSegment(cliTokenArr[0])
if err != nil {
return err, nil
}
var jHeader JwtHeader
err = json.Unmarshal(cliHeader, &jHeader)
if err != nil {
return err, nil
}
// 效验pubKey 及 token
token, err := jwt.ParseWithClaims(cliToken, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return GetRSAPublicKey(jHeader.Kid), nil
})
if err != nil {
return err, nil
}
// 信息验证
if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid {
if claims.StandardClaims.Issuer != AppleUrl || claims.StandardClaims.Audience != ClientId || claims.StandardClaims.Subject != cliUserID {
return errors.New("verify token info fail, info is not match"), nil
}
return nil, claims
}
return errors.New("token claims parse fail"), nil
}
/*
GetRSAPublicKey 向苹果服务器获取解密signature所需要用的publicKey,苹果官方
返回的公钥不止一个,可能有多个,只需要像下面一样通过和identifyToken的header里的kid
比对,找匹配到的那一个使用就行。jwt总共分三段,前两段其实只需要通过base64直接反解就可
以获取到内容了,这个也是很多同学不知道的
*/
func GetRSAPublicKey(kid string) *rsa.PublicKey {
response, err := http.Get(GetApplePublicKeys, nil, nil)
if err != nil {
return nil
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
var jKeys map[string][]JwtKeys
err = json.Unmarshal(body, &jKeys)
if err != nil {
return nil
}
// 获取验证所需的公钥
var pubKey rsa.PublicKey
// 通过cliHeader的kid比对获取n和e值 构造公钥
for _, data := range jKeys {
for _, val := range data {
if val.Kid == kid {
nByte, _ := base64.RawURLEncoding.DecodeString(val.N)
nData := new(big.Int).SetBytes(nByte)
eByte, _ := base64.RawURLEncoding.DecodeString(val.E)
eData := new(big.Int).SetBytes(eByte)
pubKey.N = nData
pubKey.E = int(eData.Uint64())
break
}
}
}
if pubKey.E <= 0 {
return nil
}
return &pubKey
}