介绍

2019年之后,对于Apple App来说,如果要支持第三方登录,则必须同时支持苹果的第三方登录,即Sign in With Apple, 本文主要介绍如何使用Go语言实现Sign in With Apple时服务端的验证, 即Generate and Validate Tokens。或者不支持第三方登录, 直接使用电话号码或者账号密码的方式进行注册以及登录。

登录流程

流程大概可以描述为:

  1. app请求通过Apple进行第三方登录,此时,客户端将会获得包括用户唯一凭证UserID(与微信的OpenId类似), 用户全名Full Name, 验证用的Code(IdentityCode)以及验证用的Token(IdentityToken)。

  2. 客户端将获得的数据发送给服务器,由服务器通过IdentityCode或者IdentityToken来验证此次登录是否有效。

  3. 如果验证通过, 服务端处理完自己内部的登录流程后, 将对应的登录结果(状态)返回给客户端。

在第二步服务器的验证过程中,服务器只需要选择Code或者Token中的任意一种进行验证即可:

client_idclient_secretredirect_uri

IdentityToken验证

.
truetrue.

一个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的步骤为:

.kidNESHA256

验证代码如下:

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
}