一、背景
QQ群里一位兄弟问到生成苹果开发平台API token的方法,我一看jwt我知道啊,不就是header、payload再用算法、秘钥签个名拼起来不就完了?
二、碰壁
-
先阅读官方文档,得知计算jwt的步骤
-
创建header,包含alg(签名算法)、typ(token类型)、kid(苹果后台获取到的私钥id),示例:
{ "alg": "ES256", // 签名算法 "kid": "2X9R4HXF34", // 苹果后台获取到的私钥id "typ": "JWT" // token类型 }
-
创建payload,包含iss(issuer ID)、exp(过期时间,unix时间戳,以秒为单位,)、aud
{ "iss": "57246542-96fe-1a63-e053-0824d011072a",// issuer ID "exp": 1528408800, // token过期时间,unix时间戳,秒为单位,必须在20分钟以内 "aud": "appstoreconnect-v1" // jwt接收方 }
-
计算签名
使用ES256算法和苹果后台提供的私钥计算签名
-
-
根据文档提示,使用JWT.io进行网页版签名计算,根据要求输入参数,但一直提示非法签名
-
使用jwt-go计算签名
-
尝试一,提示传入的可以是非法类型
package main import ( "fmt" "github.com/dgrijalva/jwt-go" "time" ) func main(){ token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ "iss": "57246542-96fe-1a63-e053-0824d011072a", "exp": time.Now().Unix(), "aud": "appstoreconnect-v1", }) token.Header["kid"] = "2X9R4HXF34" tokenString, err := token.SignedString("<privateKey>") fmt.Println(tokenString, err) } --------- key is of invalid type
-
尝试二,通过阅读源代码发现需要传入*ecdsa.PrivateKey类型的key
package main import ( "crypto/ecdsa" "fmt" "github.com/dgrijalva/jwt-go" "log" "time" ) func main(){ var ( ecdsaKey *ecdsa.PrivateKey err error ) ecdsaKey,err = jwt.ParseECPrivateKeyFromPEM([]byte("<privatekey>")) if err != nil{ log.Fatal(err) } token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ "iss": "57246542-96fe-1a63-e053-0824d011072a", "exp": time.Now().Unix(), "aud": "appstoreconnect-v1", }) token.Header["kid"] = "2X9R4HXF34" tokenString, err := token.SignedString(ecdsaKey) fmt.Println(tokenString, err) } ------------ x509: failed to parse EC private key: asn1: structure error: tags don't match (4 vs {class:0 tag:16 length:19 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} @5
-
尝试三,仔细观察下载到的privatekey文件,是以p8结尾的。在网上找到了解析p8文件的方法
package main import ( "crypto/ecdsa" "crypto/x509" "encoding/pem" "errors" "fmt" "github.com/dgrijalva/jwt-go" "log" "time" ) var ( ErrAuthKeyNotPem = errors.New("token: AuthKey must be a valid .p8 PEM file") ErrAuthKeyNotECDSA = errors.New("token: AuthKey must be of type ecdsa.PrivateKey") ErrAuthKeyNil = errors.New("token: AuthKey was nil") ) func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) { block, _ := pem.Decode(bytes) if block == nil { return nil, ErrAuthKeyNotPem } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } switch pk := key.(type) { case *ecdsa.PrivateKey: return pk, nil default: return nil, ErrAuthKeyNotECDSA } } func main() { var ( ecdsaKey *ecdsa.PrivateKey err error ) ecdsaKey, err = AuthKeyFromBytes([]byte("<privatekey>")) if err != nil { log.Fatal(err) } token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ "iss": "57246542-96fe-1a63-e053-0824d011072a", "exp": time.Now().Unix(), "aud": "appstoreconnect-v1", }) token.Header["kid"] = "2X9R4HXF34" tokenString, err := token.SignedString(ecdsaKey) fmt.Println(tokenString, err) } ------- eyJhbGciOiJFUzI1NiIsImtpZCI6IjJYOVI0SFhGMzQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhcHBzdG9yZWNvbm5lY3QtdjEiLCJleHAiOjE1NjEwODM1OTgsImlzcyI6IjU3MjQ2NTQyLTk2ZmUtMWE2My1lMDUzLTA4MjRkMDExMDcyYSJ9.E_wRsV0M7kM2aonqFBb6--Nagn5cU1mBqMOpIqJyOE34wPmcCg2-O2Ee02QbrbWHQ02rildvdiDMW8KeIWlemg <nil>
-
尝试四,签名终于计算成功了!不过还不能高兴的太早,要请求下服务器试下。请求返回401 NOT_AUTHORIZED,想死的心都有了!!!
$ curl -v -H 'Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6IjJYOVI0SFhGMzQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhcHBzdG9yZWNvbm5lY3QtdjEiLCJleHAiOjE1NjEwODM1OTgsImlzcyI6IjU3MjQ2NTQyLTk2ZmUtMWE2My1lMDUzLTA4MjRkMDExMDcyYSJ9.E_wRsV0M7kM2aonqFBb6--Nagn5cU1mBqMOpIqJyOE34wPmcCg2-O2Ee02QbrbWHQ02rildvdiDMW8KeIWlemg' "https://api.appstoreconnect.apple.com/v1/apps"
-
三、柳暗花明
经过分析代码,原来是payload里面的exp值传错了,文档中提到过期时间在未来的20分钟之内,而我传的是当前时间。token传到服务器,服务器一看,过期了,当然拒绝了。所以最后的代码:
package main
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"log"
"time"
)
var (
ErrAuthKeyNotPem = errors.New("token: AuthKey must be a valid .p8 PEM file")
ErrAuthKeyNotECDSA = errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
ErrAuthKeyNil = errors.New("token: AuthKey was nil")
)
func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(bytes)
if block == nil {
return nil, ErrAuthKeyNotPem
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
switch pk := key.(type) {
case *ecdsa.PrivateKey:
return pk, nil
default:
return nil, ErrAuthKeyNotECDSA
}
}
func main() {
var (
ecdsaKey *ecdsa.PrivateKey
err error
)
ecdsaKey, err = AuthKeyFromBytes([]byte("<privatekey>"))
if err != nil {
log.Fatal(err)
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"iss": "57246542-96fe-1a63-e053-0824d011072a",
"exp": time.Now().Add(20*time.Minute).Unix(),
"aud": "appstoreconnect-v1",
})
token.Header["kid"] = "2X9R4HXF34"
tokenString, err := token.SignedString(ecdsaKey)
fmt.Println(tokenString, err)
}