sign in with apple後端校驗(java)
最近新開發的ios平臺的app在提審的時候,被拒了,原因是app上如果有接第三方登陸(比如,微信,微博,facebook等),那就必須要接apple id登陸,坑爹~蘋果霸權啊!然而沒辦法,靠他吃飯,他是爸爸,唯有順從。下面我來說一下對接蘋果登陸的後端驗證模組,目前這一塊網上資料比較少,而且說得不夠完整。至於app端的對接,網上一搜,一大堆,很完善。
這裡先說一下apple id登陸的主要流程和涉及到的一些知識點。首先apple登陸的時序圖如下:
(來自蘋果官網:https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user)
先是app和蘋果伺服器通訊獲得identitytoken,然後把identitytoken交給業務後臺驗證,驗證通過就可以了。其中appServer涉及到的驗證,就是identitytoken,其實identitytoken就是一個jws(關於jws的只是可以參考https://www.jianshu.com/p/50ade6f2e4fd),至於校驗jws,其實是有現成的jar包可以實現,驗證jws的簽名,保證資料沒有被篡改之後,還要校驗從identitytokendecode出來的nonce,iss,aud,exp,主要是iss和exp這兩個。下面我直接上程式碼:
1.通過maven引入一下兩個包,主要是用於驗證jws,如下:
<dependency> <groupId>com.auth0</groupId> <artifactId>jwks-rsa</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>org.bitbucket.b_c</groupId> <artifactId>jose4j</artifactId> <version>0.6.4</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
2.驗證是identitytoken是否有效,其中有兩個主要的地方,第一個就是把從appleServer獲取到的publicKey字串轉換為PublicKey物件;第二個就是使用函式"jsonWebSignature.verifySignature()"驗證jws的signature,程式碼如下:
public class AppleIdAccountValidationService { private final static Logger logger = LoggerFactory.getLogger(AppleIdAccountValidationService.class); private final static int APPLE_ID_PUBLIC_KEY_EXPIRE = 24; //24h @Autowired private StringRedisUtils stringRedisUtils; public boolean isValid(String accessToken) { //校驗基本資訊:nonce,iss,aud,exp CusJws cusJws = this.getJws(accessToken); if (cusJws == null) { return false; } //iss long curTime = System.currentTimeMillis(); if (cusJws.getJwsPayload().getExp() * 1000 < curTime) { return false; } if (!JwsPayload.ISS.equals(cusJws.getJwsPayload().getIss())) { return false; } //校驗簽名 if (!this.verifySignature(accessToken)) { return false; } return true; } /** * verify signature * @param accessToken * @return */ private boolean verifySignature(String accessToken) { PublicKey publicKey = this.getAppleIdPublicKey(); JsonWebSignature jsonWebSignature = new JsonWebSignature(); jsonWebSignature.setKey(publicKey); try { jsonWebSignature.setCompactSerialization(accessToken); return jsonWebSignature.verifySignature(); } catch (JoseException e) { return false; } } /** * publicKey會本地快取1天 * @return */ private PublicKey getAppleIdPublicKey() { String publicKeyStr = stringRedisUtils.getString(Constants.REDIS_KEY_APPLE_ID_PUBLIC_KEY); if (publicKeyStr == null) { publicKeyStr = this.getAppleIdPublicKeyFromRemote(); if (publicKeyStr == null) { return null; } try { PublicKey publicKey = this.publicKeyAdapter(publicKeyStr); stringRedisUtils.setString(Constants.REDIS_KEY_APPLE_ID_PUBLIC_KEY, publicKeyStr, APPLE_ID_PUBLIC_KEY_EXPIRE, TimeUnit.HOURS); return publicKey; } catch (Exception ex) { ex.printStackTrace(); return null; } } return this.publicKeyAdapter(publicKeyStr); } /** * 將appleServer返回的publicKey轉換成PublicKey物件 * @param publicKeyStr * @return */ private PublicKey publicKeyAdapter(String publicKeyStr) { if (!StringUtils.hasText(publicKeyStr)) { return null; } Map maps = (Map)JSON.parse(publicKeyStr); List keys = (List<Map>)maps.get("keys"); Map o = (Map) keys.get(0); Jwk jwa = Jwk.fromValues(o); try { PublicKey publicKey = jwa.getPublicKey(); return publicKey; } catch (InvalidPublicKeyException e) { e.printStackTrace(); return null; } } /** * 從appleServer獲取publicKey * @return */ private String getAppleIdPublicKeyFromRemote() { ResponseEntity<String> responseEntity = new RestTemplate().getForEntity("https://appleid.apple.com/auth/keys", String.class); if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) { logger.error(String.format("getAppleIdPublicKeyFromRemote [%s] exception, detail:", appleIdPublicKeyUrl)); return null; } return responseEntity.getBody(); } private CusJws getJws(String identityToken) { String[] arrToken = identityToken.split("\\."); if (arrToken == null || arrToken.length != 3) { return null; } Base64.Decoder decoder = Base64.getDecoder(); JwsHeader jwsHeader = JSON.parseObject(new String(decoder.decode(arrToken[0])), JwsHeader.class); JwsPayload jwsPayload = JSON.parseObject(new String(decoder.decode(arrToken[1])), JwsPayload.class); return new CusJws(jwsHeader, jwsPayload, arrToken[2]); } class CusJws { private JwsHeader jwsHeader; private JwsPayload jwsPayload; private String signature; public CusJws(JwsHeader jwsHeader, JwsPayload jwsPayload, String signature) { this.jwsHeader = jwsHeader; this.jwsPayload = jwsPayload; this.signature = signature; } public JwsHeader getJwsHeader() { return jwsHeader; } public void setJwsHeader(JwsHeader jwsHeader) { this.jwsHeader = jwsHeader; } public JwsPayload getJwsPayload() { return jwsPayload; } public void setJwsPayload(JwsPayload jwsPayload) { this.jwsPayload = jwsPayload; } public String getSignature() { return signature; } public void setSignature(String signature) { this.signature = signature; } } static class JwsHeader { private String kid; private String alg; public String getKid() { return kid; } public void setKid(String kid) { this.kid = kid; } public String getAlg() { return alg; } public void setAlg(String alg) { this.alg = alg; } } static class JwsPayload { private String iss; private String sub; private String aud; private long exp; private long iat; private String nonce; private String email; private boolean email_verified; public final static String ISS = "https://appleid.apple.com"; public String getIss() { return iss; } public void setIss(String iss) { this.iss = iss; } public String getSub() { return sub; } public void setSub(String sub) { this.sub = sub; } public String getAud() { return aud; } public void setAud(String aud) { this.aud = aud; } public long getExp() { return exp; } public void setExp(long exp) { this.exp = exp; } public long getIat() { return iat; } public void setIat(long iat) { this.iat = iat; } public String getNonce() { return nonce; } public void setNonce(String nonce) { this.nonce = nonce; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public boolean isEmail_verified() { return email_verified; } public void setEmail_verified(boolean email_verified) { this.email_verified = email_verified; } } }
結束,有問題歡迎留言討論~
參考:
https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple
https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_