苹果三方登录,校验identityToken方式,golang服务端处理

苹果登录,校验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)
}

猜你喜欢

转载自blog.csdn.net/xo19882011/article/details/132188991