先说一下苹果账号登录及验证的主要流程,如下图所示
我们的后台服务器去验证客户端传递过来的信息,苹果官方提供了两种验证方式,一种是基于JWT的算法验证(这种比较简单),另外一种是基于授权码的验证。
下面主要介绍一下JWT的算法验证
一、客户端请求苹果授权登录,苹果会返回给客户端如下信息:
- userID: 授权用户的唯一标识(同一开发者账号下不同应用,同一用户的userID一样,类似微信的unionID,苹果账号没有OpenID)
- email、fullName:授权用户的信息(首次授权带有名称信息,再次授权登录苹果不会再返回名称信息)
- authorizationCode:授权码
- identityToken:授权用户的JWT凭证
identityToken示例:部分信息***遮挡
// 数据由 头部、载荷、签名 三部分组成 用 . 号分隔
"identitytoken":“eyJraWQiO***.eyJpc3MiOiJodHRwcz***.M9FvofeR8Bwc2F5n6***"
// header 解码
{"kid":"86D88Kf","alg":"RS256"}
// claims 解码
{
"iss":"https://appleid.apple.com", // 苹果签发的标识
"aud":"com.***.***", // app id or services id (对应下文的client_id)
"exp":1565668086, // 过期时间
"iat":1565667486,
"sub":"123***.123***.123**", //用户的唯一标识
}
二、如何验证上述信息
根据官方文档,只需验证以下信息:
To verify the identity token, your app server must:
* Verify the JWS E256 signature using the server’s public key
* Verify the nonce for the authentication
* Verify that the iss field contains https://appleid.apple.com
* Verify that the aud field is the developer’s client_id
* Verify that the time is earlier than the exp value of the token
百度翻译一下
要验证身份令牌,您的应用服务器必须:
* 使用服务器的公钥验证JWS E256签名
* 验证nonce身份验证
* 验证该iss字段包含https://appleid.apple.com
* 验证该aud字段是开发人员的client_id
* 验证时间早于exp令牌的值
三、使用go代码演示验证过程
项目可以直接使用jwt-go库,非常方便。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 = "com.***.***"
)
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"`
}
// 认证客户端传递过来的token是否有效
func VerifyIdentityToken(cliToken string, cliUserID string) error {
// 数据由 头部、载荷、签名 三部分组成
cliTokenArr := strings.Split(cliToken, ".")
if len(cliTokenArr) < 3 {
syslog.Logger().Errorln("cliToken Split err ! cliToken = ", cliToken)
return errors.New("cliToken Split err")
}
// 解析cliToken的header获取kid
cliHeader, err := jwt.DecodeSegment(cliTokenArr[0])
if err != nil {
syslog.Logger().Errorln(err.Error())
return err
}
var jHeader JwtHeader
err = json.Unmarshal(cliHeader, &jHeader)
if err != nil {
syslog.Logger().Errorln(err.Error())
return err
}
// 效验pubKey 及 token
token, err := jwt.ParseWithClaims(cliToken, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return GetRSAPublicKey(jHeader.Kid), nil
})
if err != nil {
syslog.Logger().Errorln(err.Error())
return err
}
// 信息验证
if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid {
if claims.Issuer != APPLE_URL || claims.Audience != APPLICATION_CLIENT_ID || claims.Subject != cliUserID {
syslog.Logger().Errorln("verify token info fail, info is not match")
return errors.New("verify token info fail, info is not match")
}
// here is verify ok !
} else {
return errors.New("token claims parse fail")
}
return nil
}
// 向苹果服务器获取解密signature所需要用的publicKey
func GetRSAPublicKey(kid string) *rsa.PublicKey {
response, err := util.HttpGet(PUBLIC_KEY_REQ_URL, nil)
if err != nil {
syslog.Logger().Errorln(err.Error())
return nil
}
var jKeys map[string][]JwtKeys
err = json.Unmarshal(response, &jKeys)
if err != nil {
syslog.Logger().Errorln(err.Error())
return nil
}
// 获取验证所需的公钥
var pubKey rsa.PublicKey
// 通过cliHeader的kid比对获取n和e值 构造公钥
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 {
syslog.Logger().Errorln("rsa.PublicKey get fail !")
return nil
}
return &pubKey
}
四、接入的过程参考了一些其他人写的文章,发现有些验证的思路不很清晰(从代码中可以看的出来)比较混乱,其实流程很简单,写出来的代码也不多。在参考其他人的文章时,需要多与官方文档比对,这样才能保证正确