Apple 登录、绑定注销

AppleUntilService:

package com.bzcst.bop.portal.third.apple.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.bzcst.bop.common.exception.BusinessException;
import com.bzcst.bop.portal.login.config.AppleConfigProperties;
import com.bzcst.bop.portal.third.apple.bo.AppleKeyResponse;
import com.bzcst.bop.portal.third.apple.vo.AppleKeyVo;
import com.bzcst.bop.portal.third.apple.vo.AppleTokenVo;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ResourceUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Apple相关
 *
 * @author liudandan
 */
@Component
@Slf4j
public class AppleUntilService {

    private static final String AUTH_TIME = "auth_time";

    @Resource
    private AppleConfigProperties appleConfig;

    @Resource
    private RestTemplate restTemplateNoSaas;

    /**
     * 获取publicKey 的算法id
     *
     * @param identityToken 苹果token的第一部分
     * @return String
     */
    public String getKid(String identityToken) {
        try {
            String[] identityTokens = identityToken.split("\\.");
            Map<String, Object> headerDate = JSONObject.parseObject(new String(Base64.decodeBase64(identityTokens[0]), StandardCharsets.UTF_8));
            return String.valueOf(headerDate.get("kid"));
        } catch (Exception e) {
            log.error("get kid fail,e={}", e);
            throw new BusinessException("apple授权登录信息验证异常");
        }
    }

    /**
     * 获取苹果的公钥信息-验证令牌签名
     *
     * @param kid
     */
    public PublicKey getPublicKey(String kid) {
        log.info("get public key start,identityToken={}", kid);
        try {
            ResponseEntity<AppleKeyResponse> responseEntity = restTemplateNoSaas.getForEntity(appleConfig.getServerUrl() + "/auth/keys", AppleKeyResponse.class);
            AppleKeyResponse appleKeyResponse = responseEntity.getBody();
            log.debug("get public key success,appleKeyResponse={}", appleKeyResponse);
            if (null == appleKeyResponse || CollectionUtils.isEmpty(appleKeyResponse.getKeys())) {
                return null;
            }
            List<AppleKeyVo> appleKeyVos = appleKeyResponse.getKeys();
            for (AppleKeyVo appleKeyVo : appleKeyVos) {
                if (kid.equals(appleKeyVo.getKid())) {
                    BigInteger modulus = new BigInteger(1, Base64.decodeBase64(appleKeyVo.getN()));
                    BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(appleKeyVo.getE()));
                    RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
                    KeyFactory kf = KeyFactory.getInstance("RSA");
                    return kf.generatePublic(spec);
                }
            }
        } catch (Exception e) {
            log.error("get public key fail,e={}", e);
            throw new BusinessException("apple授权登录信息验证异常");
        }
        return null;
    }

    /**
     * 解密个人信息
     *
     * @param identityToken APP获取的identityToken
     * @return 解密参数:失败返回null  sub就是用户id,用户昵称需要前端传过来
     */
    public AppleTokenVo getAppleUserInfo(String identityToken) {
        try {
            String[] identityTokens = identityToken.split("\\.");
            Map<String, Object> headerDate = JSONObject.parseObject(new String(Base64.decodeBase64(identityTokens[0]), StandardCharsets.UTF_8));
            Map<String, Object> payloadData = JSONObject.parseObject(new String(Base64.decodeBase64(identityTokens[1]), StandardCharsets.UTF_8));
            AppleTokenVo appleTokenVo = JSON.parseObject(JSON.toJSONString(payloadData), AppleTokenVo.class);
            appleTokenVo.setKid(String.valueOf(headerDate.get("kid")));
            appleTokenVo.setAlg(String.valueOf(headerDate.get("alg")));
            return appleTokenVo;
        } catch (Exception e) {
            log.info("get apple user information fail,e={} ", e);
            throw new BusinessException("apple授权登录信息验证异常");
        }
    }

    /**
     * 账号注销
     *
     * @param code
     * @param clientId
     * @return
     */
    public Boolean userRevoke(String code, String clientId) {
        try {
            return true;
        } catch (Exception e) {
            log.info("get apple user information fail,e={} ", e);
            throw new BusinessException("apple账号注销异常");
        }
    }

    /**
     * 验证
     *
     * @param publicKey     苹果的公钥
     * @param identityToken APP获取的identityToken
     * @param sub           用户的唯一标识符对应APP获取到的:user
     * @return true/false
     */
    public boolean verifyIdentityToken(PublicKey publicKey, String identityToken, String sub) {
        try {
            JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
            jwtParser.requireIssuer(appleConfig.getServerUrl());
            //Apple Developer帐户中的client_id
            jwtParser.requireAudience(appleConfig.getClientId());
            jwtParser.requireSubject(sub);
            Jws<Claims> claim = jwtParser.parseClaimsJws(identityToken);
            if (claim != null && claim.getBody().containsKey(AUTH_TIME)) {
                return true;
            }
        } catch (ExpiredJwtException e1) {
            log.error("apple token verify fail,identityToken is expired!");
            throw new BusinessException("apple授权登录信息已过期");
        } catch (Exception e2) {
            log.error("apple token verify fail,error={}", e2);
            throw new BusinessException("非法的apple授权登录信息");
        }
        return false;
    }

    /**
     * 根据authorizationCode授权码来获取refreshToken
     *
     * @param authorizationCode 客户端授权code
     * @return
     */
    public String generateToken(String authorizationCode) {
        String refreshToken = null;
        try {
            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.add("client_id", appleConfig.getClientId());
            map.add("client_secret", generateClientSecret());
            map.add("grant_type", "authorization_code");
            map.add("code", authorizationCode);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
            String value = restTemplateNoSaas.postForEntity(appleConfig.getServerUrl() + "/auth/token", request, String.class).getBody();
            if (StringUtils.isNotBlank(value)) {
                JSONObject json = JSONObject.parseObject(value);
                refreshToken = json.getString("refresh_token");
            }
        } catch (HttpClientErrorException e) {
            throw new BusinessException(e.getResponseBodyAsString());
        } catch (Exception e) {
            throw new BusinessException(e.getMessage());
        }
        return refreshToken;
    }

    /**
     * 生成客户端密钥
     */
    private String generateClientSecret() throws Exception {
        Map<String, Object> header = new HashMap<>();
        header.put("kid", appleConfig.getKeyId()); //P8文件下载得到的Key ID
        header.put("alg", SignatureAlgorithm.ES256.getValue()); //SHA256
        Map<String, Object> claims = new HashMap<>();
        long now = System.currentTimeMillis() / 1000;
        claims.put("iss", appleConfig.getTeamId());
        claims.put("iat", now);
        claims.put("exp", now + 648000); // 最长半年,单位秒
        claims.put("aud", appleConfig.getServerUrl());
        claims.put("sub", appleConfig.getClientId());
        File file = null;
        try {
            file = ResourceUtils.getFile("classpath:AuthKey_T.xxxxxxxxx.p8");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        BufferedReader br = new BufferedReader(new FileReader(file));
        String string;
        StringBuffer sb = new StringBuffer();
        while ((string = br.readLine()) != null) {
            if (string.startsWith("---")) {
                continue;
            }
            sb.append(string);
        }
        br.close();
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(sb.toString().replaceAll("\\n", "")));
        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        return Jwts.builder().setHeader(header).setClaims(claims).signWith(SignatureAlgorithm.ES256, privateKey).compact();
    }

    /**
     * 根据refreshToken刷新accessToken
     */
    private String refreshToken(String refreshToken) {
        String accessToken = null;
        try {
            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.add("client_id", appleConfig.getClientId());
            map.add("client_secret", generateClientSecret());
            map.add("grant_type", "refresh_token");
            map.add("refresh_token", refreshToken);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
            String value = restTemplateNoSaas.postForEntity(appleConfig.getServerUrl() + "/auth/token", request, String.class).getBody();
            if (StringUtils.isNotBlank(value)) {
                JSONObject json = JSONObject.parseObject(value);
                accessToken = json.getString("access_token");
            }
        } catch (HttpClientErrorException e) {
            throw new BusinessException(e.getResponseBodyAsString());
        } catch (Exception e) {
            throw new BusinessException(e.getMessage());
        }
        log.info("刷新token成功");
        return accessToken;
    }

    /**
     * 撤销用户token
     */
    public boolean revokeToken(String appleRefreshToken) {
        try {
            //更新accessToken
            String accessToken = refreshToken(appleRefreshToken);
            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.add("client_id", appleConfig.getClientId());
            map.add("client_secret", generateClientSecret());
            map.add("token_type_hint", "access_token");
            map.add("token", accessToken);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
            ResponseEntity<String> values = restTemplateNoSaas.postForEntity(appleConfig.getServerUrl() + "/auth/revoke", request, String.class);
            //Apple官网上请求只有200与400,这里没有抛异常,证明是成功注销的
            if (values.getStatusCode() == HttpStatus.OK) {
                log.info("apple账户注销成功");
                return true;
            }
            log.info("apple账户注销失败");
            return false;
        } catch (HttpClientErrorException e) {
            throw new BusinessException(e.getResponseBodyAsString());
        } catch (Exception e) {
            throw new BusinessException(e.getMessage());
        }
    }


}

AppleConfigProperties

package com.bzcst.bop.portal.login.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * apple配置
 *
 * @author liudandan
 */
@Data
@ConfigurationProperties(prefix = "apple-config")
@Component
public class AppleConfigProperties {

    /**
     * 请求url地址
     */
    private String serverUrl;

    /**
     * 请求url地址
     */
    private String teamId;

    /**
     * clientId
     */
    private String clientId;

    /**
     * keyId
     */
    private String keyId;


    /**
     * validityPeriod
     */
    private String validityPeriod;

    /**
     * 获取apple的PrivateKey
     *
     * @return
     */
    public String getApplePrivateKey() {
        //TODO::从资源文件拿取配置文件
        return "";
    }
}

AppleUntilService:

apple-config:
  serverUrl: https://appleid.apple.com
  teamId: xxxxxxxxxx
  clientId: xxxxxxxxxx
  keyId: xxxxxxxxxx
  validityPeriod: 180
posted @ 2022-08-17 10:15  西门长海  阅读(224)  评论(0编辑  收藏  举报