最近新開發的ios平台的app在提審的時候,被拒了,原因是app上如果有接第三方登陸(比如,微信,微博,facebook等),那就必須要接apple id登陸,坑爹~蘋果霸權啊!然而沒辦法,靠他吃飯,他是爸爸,唯有順從。下面我來說一下對接蘋果登陸的后端驗證模塊,目前這一塊網上資料比較少,而且說得不夠完整。至於app端的對接,網上一搜,一大堆,很完善。
這里先說一下apple id登陸的主要流程和涉及到的一些知識點。首先apple登陸的時序圖如下:

先是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;
}
}
}
warn:以上是后台的驗證方式一,后來發現有問題,更新后的方案以及后端驗證的第二種方式,統一在微信公眾號“ismallboy”更新。
歡迎關注微信公眾號“ismallboy”,請掃碼並關注以下公眾號,並在公眾號下面回復“word”,獲得本文最新內容。

