sign in with apple后端校验(java)
最近新开发的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”,获得本文最新内容。