Sign in with Apple(蘋果授權登陸)服務端驗證

此文章來源自網(wǎng)絡,目的在于方便廣大開發(fā)者,轉(zhuǎn)載請注明原文出處,感謝。

文章來源

蘋果授權登陸方式

1、PC/M端授權登陸,采用協(xié)議類似于oauth2協(xié)議
2、 App端授權登陸,提供兩種后端驗證方式

開發(fā)者后臺配置

詳細配置參考該文檔,手把手教學
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple

1、 PC/M接入方式

后臺配置請參考下方文檔,其中client_id對應的是Services ID,redirect_uri就是后臺配置的接收code碼的地址
https://appleid.apple.com/auth/authorize?response_type=code&client_id=&redirect_uri=&state=1234

2、APP端客戶端授權登陸功能開發(fā)

可以參考如下文檔,重點講解蘋果授權登陸后端如何驗證
http://www.itdecent.cn/p/23b46dea2076

針對后端驗證蘋果提供了兩種驗證方式,一種是基于JWT的算法驗證,另外一種是基于授權碼的驗證
1、基于JWT的算法驗證

//接口返回值
{  
"keys": [
    {
      "kty": "RSA", 
      "kid": "AIDOPK1",
      "use": "sig",
      "alg": "RS256",
      "n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
      "e": "AQAB"
    }  
]
}

kid,為密鑰id標識,簽名算法采用的是RS256(RSA 256 + SHA 256),kty常量標識使用RSA簽名算法,其公鑰參數(shù)為n和e,其值采用了BASE64編碼,使用時需要先解碼

  • 使用方式:APP內(nèi)蘋果授權登陸會提供如下幾個參數(shù):userID、email、fullName、authorizationCode、identityToken
    • userID:授權的用戶唯一標識
    • email、fullName:授權的用戶資料
    • authorizationCode:授權code
    • identityToken:授權用戶的JWT憑證
下面針對identityToken后端驗證做簡要說明:

identityToken參考樣例:

// jwt 格式 該token的有效期是10分鐘
eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuZGV2aWNlbW9uaXRvciIsImV4cCI6MTU2NTY2ODA4NiwiaWF0IjoxNTY1NjY3NDg2LCJzdWIiOiIwMDEyNDcuOTNiM2E3OTlhN2M4NGMwY2I0NmNkMDhmMTAwNzk3ZjIuMDcwNCIsImNfaGFzaCI6Ik9oMmFtOWVNTldWWTNkcTVKbUNsYmciLCJhdXRoX3RpbWUiOjE1NjU2Njc0ODZ9.e-pdwK4iKWErr_Gcpkzo8JNi_MWh7OMnA15FvyOXQxTx0GsXzFT3qE3DmXqAar96nx3EqsHI1Qgquqt2ogyj-lLijK_46ifckdqPjncTEGzVWkNTX8uhY7M867B6aUnmR7u-cf2HsmhXrvgsJLGp2TzCI3oTp-kskBOeCPMyTxzNURuYe8zabBlUy6FDNIPeZwZXZqU0Fr3riv2k1NkGx5MqFdUq3z5mNfmWbIAuU64Z3yKhaqwGd2tey1Xxs4hHa786OeYFF3n7G5h-4kQ4lf163G6I5BU0etCRSYVKqjq-OL-8z8dHNqvTJtAYanB3OHNWCHevJFHJ2nWOTT3sbw
 
// header 解碼
{"kid":"AIDOPK1","alg":"RS256"} 其中kid對應上文說的密鑰id
 
// claims 解碼
{
"iss":"https://appleid.apple.com",  // 蘋果簽發(fā)的標識
"aud":"com.skyming.devicemonitor", // 接收者的APP ID
"exp":1565668086,"iat":1565667486,
"sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704", //用戶的唯一標識
"c_hash":"Oh2am9eMNWVY3dq5JmClbg",
"auth_time":1565667486
}

其中 iss標識是蘋果簽發(fā)的,aud是接收者的APP ID,該token的有效期是10分鐘,sub就是用戶的唯一標識

如何驗證?
#首先通過identityToken中的header中的kid,然后結(jié)合蘋果獲取公鑰的接口,拿到相應的n和e的值,然后通過下面這個方法構(gòu)建RSA公鑰
public RSAPublicKeySpec build(String n, String e) {  
    BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
    BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
    return new RSAPublicKeySpec(modulus, publicExponent);    
}
 
#獲取驗證所需的PublicKey
public PublicKey getPublicKey(String n,String e)throws NoSuchAlgorithmException, InvalidKeySpecException {
         BigInteger bigIntModulus = new BigInteger(1,Base64.decodeBase64(n));
         BigInteger bigIntPrivateExponent = new BigInteger(1,Base64.decodeBase64(e));
        RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        return publicKey;
 }

#通過下面這個方法驗證JWT的有效性
# jwt 就是 identityToken:授權用戶的JWT憑證
# audience就是APPID
# subject 就是 就是userId
public int verify(PublicKey key, String jwt, String audience, String subject) {                      
    JwtParser jwtParser = Jwts.parser().setSigningKey(key);              
    jwtParser.requireIssuer("https://appleid.apple.com");        
    jwtParser.requireAudience(audience);
    jwtParser.requireSubject(subject); 
    try {
       Jws<Claims> claim = jwtParser.parseClaimsJws(jwt);
       if (claim != null && claim.getBody().containsKey("auth_time")) {  
          return GlobalCode.SUCCESS;            
       }           
       return GlobalCode.THIRD_AUTH_CODE_INVALID;
    } catch (ExpiredJwtException e) { 
       log.error("apple identityToken expired", e);
       return GlobalCode.THIRD_AUTH_CODE_INVALID;
    } catch (Exception e) {
       log.error("apple identityToken illegal", e);
       return GlobalCode.FAIL_ILLEGAL_REQ;
    }
}
 
#使用的JWT工具庫為:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
2、基于授權碼的后端驗證

首先需要了解如何構(gòu)建client_secret,詳細文檔可以參考如下兩個:
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens

  • 首先說下client_secret的構(gòu)建方法:
    先在后臺生成授權應用APP ID的密鑰KEY文件,然后下載密鑰文件,此文件只能下載一次,請妥善保存,格式樣例:
#密鑰KEY格式樣例
-----BEGIN PRIVATE KEY-----
  BASE64編碼后的密鑰
-----END PRIVATE KEY-----

#秘鑰讀取
public  byte[] readKey() throws Exception {
   String temp = "密鑰文件中間的編碼字符串";
   return Base64.decodeBase64(temp);
}

#構(gòu)建client_secret關鍵代碼:
String client_id = "..."; // 被授權的APP ID
Map<String, Object> header = new HashMap<String, Object>();
header.put("kid", "密鑰id"); // 參考后臺配置
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("iss", "team id"); // 參考后臺配置 team id
long now = System.currentTimeMillis() / 1000;
claims.put("iat", now);
claims.put("exp", now + 86400 * 30); // 最長半年,單位秒
claims.put("aud", "https://appleid.apple.com"); // 默認值
claims.put("sub", client_id);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readKey());
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
String client_secret = Jwts.builder().setHeader(header).setClaims(claims).signWith(SignatureAlgorithm.ES256, privateKey).compact();         

如何驗證?
String url = "https://appleid.apple.com/auth/token";
// POST 請求
HttpSynClient client = new HttpSynClient(5000, 5000, 5000, 20);
Map<String, String> form = new HashMap<String, String>();
form.put("client_id", client_id);
form.put("client_secret", client_secret);
form.put("code", code);form.put("grant_type","authorization_code");
form.put("redirect_uri", redirectUrl);
HttpResponse result = client.excutePost(url, form);
System.out.println(result);
返回值樣例:
{
"access_token":"a0996b16cfb674c0eb0d29194c880455b.0.nsww.5fi5MVC-i3AVNhddrNg7Qw",
"token_type":"Bearer",
"expires_in":3600,
"refresh_token":"r9ee922f1c8b048208037f78cd7dfc91a.0.nsww.KlV2TeFlTr7YDdZ0KtvEQQ",
"id_token":"eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA"
}

其中id_token是一個JWT,其中claims中的sub就是授權的用戶唯一標識,該token也可以使用上述的驗證方法進行有效性驗證,另外授權code是有時效性的,且使用一次即失效

擴展資料
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容