zoukankan      html  css  js  c++  java
  • IOS苹果登录sign in with apple后端校验

      最近新开发的app在IOS平台app store connent提审的时候,被拒了,原因是app上如果有接第三方登陆(比如微信,微博,facebook等),那就必须要接apple id登陆,坑爹~苹果霸权啊!然而没办法,所以只能接入苹果登录。

      APP端的接入可以看上一篇博客:iOS苹果授权登录(Sign in with Apple)/Apple登录/苹果登录集成教程,下面我来说一下对接苹果登陆的后端验证模块。

      这里先说一下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这两个。

      针对后端验证苹果提供了两种验证方式,一种是基于JWT的算法验证,另外一种是基于授权码的验证。

    一、苹果登录JAVA后台校验:JWT的identityToken验证模式

    //接口返回值
    {  
    "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签名算法,其公钥参数为n和e,其值采用了BASE64编码,使用时需要先解码

    • 使用方式:APP内苹果授权登陆会提供如下几个参数:userID、email、fullName、authorizationCode、identityToken
      • userID:授权的用户唯一标识
      • email、fullName:授权的用户资料
      • authorizationCode:授权code
      • identityToken:授权用户的JWT凭证

      下面针对identityToken后端验证做简要说明,话不多说直接上代码:

    1、app端请求appleServer返回的identityTokenn参考样例

    "identityToken":"ZXlKcmFXUWlPaUpsV0dGMWJtMU1JaXdpUndje***xMXpZZ3BiYWRIWHdGVEtR4ejRPZTBhUkdtcHZOZFpWVkJGQjN4OU13"
    // 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",  // 苹果签发的标识
    "aud":"com.skyming.devicemonitor", // 接收者的APP ID
    "exp":1565668086,"iat":1565667486,
    "sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704", //用户的唯一标识
    "c_hash":"Oh2am9eMNWVY3dq5JmClbg",
    "auth_time":1565667486
    }

      其中 iss标识是苹果签发的,aud是接收者的APP ID,该token的有效期是10分钟,sub就是用户的唯一标识

      如何验证呢?

    //首先通过identityToken中的header中的kid,然后结合苹果获取公钥的接口,拿到相应的n和e的值,然后通过下面这个方法构建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、完整校验代码:

    (1)有2次验证,分别调取apple提供的接口和标识

    private final String APPLE_AUTH_URL = "https://appleid.apple.com/auth/keys";
    private final String ISS = "https://appleid.apple.com";

    (2)一次是利用token获取解密的publicKey;另一次就是再校验这个publicKey及token。

      verify(AppleLoginVO appleLoginVO)里解析token

      然后getPublicKey(kid)获取publicKey

      然后再verify(publicKey, identityToken, aud, sub)校验

    (3)封装的httpclient请求:private JSONObject getHttp(String url)

      完整示例代码:

    @Slf4j
    @Service
    public class AppleLoginService {
        private final String APPLE_AUTH_URL = "https://appleid.apple.com/auth/keys";
    
        private final String ISS = "https://appleid.apple.com";
    
        public boolean verify(AppleLoginVO appleLoginVO) {
            //这里传过来的identityToken应该是三个.分割,解密之后
            String identityToken = appleLoginVO.getIdentityToken();
            try {
                if (identityToken.split("\.").length > 1){
                    String firstDate = new String( Base64.decodeBase64(identityToken.split("\.")[0]),"UTF-8");
                    String claim = new String(Base64.decodeBase64(identityToken.split("\.")[1]), "UTF-8");
                    String kid = JSONObject.parseObject(firstDate).get("kid").toString();
                    String aud = JSONObject.parseObject(claim).get("aud").toString();
                    String sub = JSONObject.parseObject(claim).get("sub").toString();
                    PublicKey publicKey = getPublicKey(kid);
                    if (publicKey == null) {
                        log.error("Apple have no info data!");
                        return false;
                    }
                    boolean reuslt = verify(publicKey, identityToken, aud, sub);
                    if (reuslt) {
                        log.info("苹果登录授权成功");
                        return true;
                    }
                }
            } catch (Exception e) {
                log.error("苹果登录授权异常:", LogUtil.getStack(e));
                e.printStackTrace();
            }
            log.error("identityToken格式不正确");
            return false;
        }
    
        private PublicKey getPublicKey(String kid) {
            try {
                JSONObject debugInfo = getHttp(APPLE_AUTH_URL);
                if (debugInfo == null) {
                    return null;
                }
                JSONObject jsonObject = debugInfo.getJSONObject("body");
                String keys = jsonObject.getString("keys");
                JSONArray jsonArray = JSONObject.parseArray(keys);
                if (jsonArray.isEmpty()) {
                    return null;
                }
                for (Object object : jsonArray) {
                    JSONObject json = ((JSONObject) object);
                    if (json.getString("kid").equals(kid)) {
                        String n = json.getString("n");
                        String e = json.getString("e");
                        BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
                        BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
                        RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
                        KeyFactory kf = KeyFactory.getInstance("RSA");
                        return kf.generatePublic(spec);
                    }
                }
            } catch (Exception e) {
                log.error("getPublicKey异常!", LogUtil.getStack(e));
                e.printStackTrace();
            }
            return null;
    
        }
    
        private boolean verify(PublicKey key, String jwt, String audience, String subject){
            boolean result = false;
            JwtParser jwtParser = Jwts.parser().setSigningKey(key);
            jwtParser.requireIssuer(ISS);
            jwtParser.requireAudience(audience);
            jwtParser.requireSubject(subject);
            try {
                Jws<Claims> claim = jwtParser.parseClaimsJws(jwt);
                if (claim != null && claim.getBody().containsKey("auth_time")) {
                    return true;
                }
            } catch (ExpiredJwtException e) {
                log.error("getPublicKey异常{苹果identityToken过期}", LogUtil.getStack(e));
            } catch (SignatureException e) {
                log.error("getPublicKey异常{苹果identityToken非法}", LogUtil.getStack(e));
            }
            return result;
        }
    
        private JSONObject getHttp(String url) {
            log.info("[请求地址]: " + url);
            JSONObject resultJson = new JSONObject();
            resultJson.put("code", -1);
            CloseableHttpClient httpclient = HttpClients.createDefault();
            try {
                HttpGet httpPost = new HttpGet(url);
                CloseableHttpResponse response = httpclient.execute(httpPost);
                try {
    
                    HttpEntity entity = response.getEntity();
                    if (response.getStatusLine().getStatusCode() == 200) {
                        String resp = EntityUtils.toString(entity);
                        resultJson.put("code", 0);
                        resultJson.put("body", JSONObject.parseObject(resp));
                    } else {
                        resultJson.put("code", response.getStatusLine().getStatusCode());
                        log.error("[错误码] :" + response.getStatusLine().getStatusCode());
                        log.error("[请求地址] :" + url);
                    }
    
                } finally {
                    response.close();
                }
            } catch (ClientProtocolException e) {
                log.error("[异常] :", LogUtil.getStack(e));
            } catch (IOException e) {
                log.error("[异常] :", LogUtil.getStack(e));
            } finally {
                try {
                    httpclient.close();
                } catch (IOException e) {
                    log.error("[httpclient 关闭异常] : ", LogUtil.getStack(e));
                }
            }
            return resultJson;
        }
    }
  • 相关阅读:
    【算法】动态规划
    【设计模式】单例模式
    Python 多元线性回归
    Python 线性回归
    惩罚项
    局部常数拟合方法 例
    微分方程是用来做什么的?
    线性回归与梯度下降法
    k近邻法
    逻辑回归与梯度下降法
  • 原文地址:https://www.cnblogs.com/goloving/p/14349122.html
Copyright © 2011-2022 走看看