首页 » ios付费应用 » C端APP微信授权登录,审核被拒了,怎么办?

C端APP微信授权登录,审核被拒了,怎么办?

 

前言:公司C端APP刚刚做了微信授权登录,第一版也已经成功上线。第二版APP被拒绝是因为苹果规定只要APP连接第三方社交平台的授权登录必须接受他的苹果账号授权登录,否则审核可能会被拒绝...接受它,你能放下它吗! (我们考虑过加一个服务器和一个交换机,提交审核前应该隐藏第三方授权登录功能,审核通过后开启。听IOS同事说有一个冒着被封号的风险,我放弃了登陆苹果id怎么取消验证码,然后开始看苹果的官方文档……)

一、对接过程;苹果的官方文档很难描述。也许外国人的思维方式和我们不一样?可能是我的英语水平太低了。看了中文文档,还是觉得还是不知道整个过程。后来找了一个帖子,供搜索引擎参考。

1、先看时序图。苹果的授权登录与微信有些不同。 APP调用接口获取类似微信的信息,然后传输到服务器。服务器验证授权信息。如果有效(校验或),如果有效则可以进行后续的逻辑处理(一般是注册用户或者缓存授权信息以备下次操作):

服务器验证授权登录的有效性有两种方式。上图为验证。本文只讲这种实现方式(实现比较简单,也有最简单的一种登陆苹果id怎么取消验证码,就是服务端不Check,哈哈_)。

2、登录开发者网站,开启应用授权登录功能:

在这里插入图片描述

​​​​

3、APP前端连接苹果对应接口获取授权信息。字段说明请参考以下链接:

使用 Apple 登录详细信息登录

登录方式

App 登录授权(Java 后端认证)

值得一提的是,他是一个签名的 Json Web Token (JWT)

主要是三件事:

:加密算法是通过这个对象获取的,具体使用的公钥是从这里获取的。有两个苹果公钥,随机的

:传输的数据,这里主要是一些授权信息

:前两部分的签名(,)

二、java服务器实现代码:

1、需要导入的jar:

		<!-- 苹果账号授权登录依赖的jar -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.1</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.1</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.11.1</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>2.9.8</version>
		</dependency>

2、令牌解析工具类:

package com.lieni.apple.utils;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lieni.apple.bean.AppleJwtPayloadBean;
import com.lieni.core.util.JsonUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
public class IdentityTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(IdentityTokenUtil.class);
    public final static String APP_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys";
    private final static String APPLE_ISSUER_URL = "https://appleid.apple.com";
    private final static String APPLE_AUD = "你的APP ID";
    public final static String APPLE_PRIVATE_EMAIL_ADDRESS = "@privaterelay.appleid.com";
    /**
     * 通过下面这个方法验证identityToken的有效性:授权用户的JWT凭证
     *
     * @param identityToken
     * @return
     */
    public static boolean verify(String identityToken, PublicKey publicKey) {
        LOGGER.info("identityToken=[{}]", identityToken);
        try {
            if (publicKey == null) {
                LOGGER.error("publicKey can not be null.");
                return false;
            }
            // 3 验证 https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
            JwtParser jwtParser = Jwts.parserBuilder().requireIssuer(APPLE_ISSUER_URL).requireAudience(APPLE_AUD)
                    .setSigningKey(publicKey).build();
            Jws<Claims> claim = jwtParser.parseClaimsJws(identityToken);
            if (claim != null) {
                LOGGER.debug("claim=[{}]", claim);
                String sub = claim.getBody().get("sub").toString();// sub,即用户的Apple的openId
                String iss = claim.getBody().get("iss").toString();
                String aud = claim.getBody().get("aud").toString();
                long exp = Long.valueOf(claim.getBody().get("exp").toString()) * 1000;// exp is second
                return APPLE_ISSUER_URL.equals(iss) && APPLE_AUD.equals(aud) && System.currentTimeMillis() < exp;
            }
            return false;
        } catch (ExpiredJwtException e) {
            LOGGER.error("apple identityToken expired", e);
            return false;
        } catch (Exception e) {
            LOGGER.error("apple identityToken illegal", e);
            return false;
        }
    }
    public static String getKidByIdentityToken(String identityToken) {
        if (StringUtils.isBlank(identityToken)) {
            return null;
        }
        Map<String, JSONObject> map = parserIdentityToken(identityToken);
        if (MapUtils.isEmpty(map)) {
            return null;
        }
        JSONObject header = map.get("header");
        return header != null ? header.getString("kid") : null;
    }
    /**
     * 对前端传来的JWT字符串identityToken的第二部分进行解码 主要获取其中的aud和sub,aud对应ios前端的包名,sub对应当前用户的授权openID
     *
     * @param identityToken
     * @return
     */
    private static Map<String, JSONObject> parserIdentityToken(String identityToken) {
        Map<String, JSONObject> map = new HashMap<>();
        String[] arr = identityToken.split("\.");
        String deHeader = new String(Base64.decodeBase64(arr[0]));
        LOGGER.info("header=[{}]", deHeader);
        JSONObject header = JSON.parseObject(deHeader);
        map.put("header", header);
        String dePayload = new String(Base64.decodeBase64(arr[1]));
        LOGGER.info("dePayload=[{}]", dePayload);
        JSONObject payload = JSON.parseObject(dePayload);
        map.put("payload", payload);
        return map;
    }
    public static AppleJwtPayloadBean getPayloadByIdentityToken(String identityToken) {
        String[] arr = identityToken.split("\.");
        if (ArrayUtils.isEmpty(arr)) {
            return new AppleJwtPayloadBean();
        }
        String dePayload = new String(Base64.decodeBase64(arr[1]));
        if (StringUtils.isBlank(dePayload)) {
            return new AppleJwtPayloadBean();
        }
        return JsonUtil.toObject(dePayload, AppleJwtPayloadBean.class);
    }
    public static PublicKey getPublicKey(Map<String, String> keyMap) {
        if (MapUtils.isEmpty(keyMap)) {
            LOGGER.error("keyMap ca not be empty.");
            return null;
        }
        // 首先通过identityToken中的header中的kid,然后结合苹果获取公钥的接口,拿到相应的n和e的值,然后通过下面这个方法构建RSA公钥
        BigInteger modulus = new BigInteger(1, Base64.decodeBase64(keyMap.get("n")));
        BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(keyMap.get("e")));
        RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
        try {
            return KeyFactory.getInstance("RSA").generatePublic(keySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            LOGGER.error("get publicKey error.", e);
        }
        return null;
    }
    public static void main(String[] args) {
        String identityToken = "eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiTGllbmkuaU9TLUMiLCJleHAiOjE1OTUzMjA5MzksImlhdCI6MTU5NTMyMDMzOSwic3ViIjoiMDAwNjUyLmQ4MDNjZDNlNmEzZjRkZmJiNjhlOTNjMTU5OTNkZTUzLjA3MTQiLCJjX2hhc2giOiJBbUY2QzFJZWFfMXJhQUdtQTUtMmFRIiwiZW1haWwiOiJrbXdpZWF3YzNzQHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTk1MzIwMzM5LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.POGQEq7Ccy9WqRfs92-wccxiz4q7jhZdKUnhaQiSbYy4nSRU-UEQq1kyoCdjR9Knyt1_sAUbQ5T6UBWQ6q4eOYpBNk37ZGcsnEK4_iBSO5H2cPkytT_ZXqIrUykJ1Iti9QIuYA0Y53pg-ZNBsFlRUIHG_b5OFMQmLoyCojIUwNDLSBV0KXR24rwMnDNMNCcuv1xPk26IvhxAvKCKQfOyNgdxx7ByhiW_qX7vlfVT1YqTPXjFEl24CaUwHBCOcYywSoQEprwP2-ZbCaIvEs0MkGGNTdtnUKAq2_O8Z-RShaqZ2gOtkKcFhfzZeFV8DouIJccXoEx9_2s7aFBAc5tLVw";
        // boolean verify = IdentityTokenUtil.verify(identityToken);
        System.out.println(JsonUtil.toJson(getPayloadByIdentityToken(identityToken)));
    }
}

3、提供认证服务的本地服务(缓存苹果的公钥):

@Service
public class AppleLoginService {
    private static final Logger LOGGER = LoggerFactory.getLogger(AppleLoginService.class);
    @Resource(name = "jdkRedisTemplate")
    private RedisTemplate<String, Map<String, String>> jdkRedisTemplate;
    public boolean verify(String identityToken) {
        return IdentityTokenUtil.verify(identityToken,
                getPublicKey(IdentityTokenUtil.getKidByIdentityToken(identityToken)));
    }
    /**
     * 获取验证所需的PublicKey
     *
     * @param kid
     * @return
     */
    private PublicKey getPublicKey(String kid) {
        return IdentityTokenUtil.getPublicKey(mapApplePublicKeyByKid(kid));
    }
    @SuppressWarnings("unchecked")
    private Map<String, String> mapApplePublicKeyByKid(String kid) {
        if (jdkRedisTemplate.hasKey(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey())) {
            Object values = jdkRedisTemplate.opsForHash().get(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey(),
                    kid);
            if (!(values instanceof Map)) {
                return Collections.emptyMap();
            }
            return (Map<String, String>) values;
        }
        Map<String, Map<String, String>> map = new HashMap<>();
        try {
            String responseBody = HttpCaller.get(HttpClients.createDefault(), IdentityTokenUtil.APP_PUBLIC_KEYS_URL,
                    null, "UTF-8");
            if (StringUtils.isBlank(responseBody)) {
                return Collections.emptyMap();
            }
            LOGGER.debug("responseBody=[{}]", responseBody);
            JSONObject jsonObject = JSONObject.parseObject(responseBody);
            if (null == jsonObject) {
                return Collections.emptyMap();
            }
            JSONArray keys = jsonObject.getJSONArray("keys");
            for (int i = 0; i < keys.size(); i++) {
                Map<String, String> m = new HashMap<>();
                m.put("n", keys.getJSONObject(i).getString("n"));
                m.put("e", keys.getJSONObject(i).getString("e"));
                map.put(keys.getJSONObject(i).getString("kid"), m);
            }
            // 加缓存
            jdkRedisTemplate.opsForHash().putAll(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey(), map);
            jdkRedisTemplate.expire(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey(), 24, TimeUnit.HOURS);
            return map.get(kid);
        } catch (IOException e) {
            LOGGER.error("mapApplePublicKeyByKid error.", e);
            return Collections.emptyMap();
        }
    }
}

因为赶着上线,上面的代码还是比较粗糙的。码友来看看吧。将在后续项目中进行优化。以上代码已经通过亲测验证,大家可以放心使用了!

节点/小火箭/美区ID/国外苹果ID/美区小火箭购买/美区小火箭兑换码/shadowrocket兑换码/苹果商店下载shadowrocket网址

原文链接:C端APP微信授权登录,审核被拒了,怎么办?,转载请注明来源!

0