go服务器验证苹果账号登录

先说一下苹果账号登录及验证的主要流程,如下图所示

App AppleServer AppServer get identityToken 返回 identityToken 提交identityToken和userInfo 获取解密signature的publicKey 返回publicKey Appserver使用publicKey验证identityToken, 验证通过的话就使用userInfo创建本地用户 返回新创建 的本地用户信息 App AppleServer AppServer

我们的后台服务器去验证客户端传递过来的信息,苹果官方提供了两种验证方式,一种是基于JWT的算法验证(这种比较简单),另外一种是基于授权码的验证。

下面主要介绍一下JWT的算法验证

官方参考文档: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user

一、客户端请求苹果授权登录,苹果会返回给客户端如下信息:

  • 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
}

四、接入的过程参考了一些其他人写的文章,发现有些验证的思路不很清晰(从代码中可以看的出来)比较混乱,其实流程很简单,写出来的代码也不多。在参考其他人的文章时,需要多与官方文档比对,这样才能保证正确

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值