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;
@Component
@Slf4j
public class AppleUntilService {
private static final String AUTH_TIME = "auth_time";
@Resource
private AppleConfigProperties appleConfig;
@Resource
private RestTemplate restTemplateNoSaas;
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授权登录信息验证异常");
}
}
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;
}
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授权登录信息验证异常");
}
}
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账号注销异常");
}
}
public boolean verifyIdentityToken(PublicKey publicKey, String identityToken, String sub) {
try {
JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
jwtParser.requireIssuer(appleConfig.getServerUrl());
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;
}
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());
header.put("alg", SignatureAlgorithm.ES256.getValue());
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();
}
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;
}
public boolean revokeToken(String appleRefreshToken) {
try {
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);
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;
@Data
@ConfigurationProperties(prefix = "apple-config")
@Component
public class AppleConfigProperties {
private String serverUrl;
private String teamId;
private String clientId;
private String keyId;
private String validityPeriod;
public String getApplePrivateKey() {
return "";
}
}
AppleUntilService:
apple-config:
serverUrl: https://appleid.apple.com
teamId: xxxxxxxxxx
clientId: xxxxxxxxxx
keyId: xxxxxxxxxx
validityPeriod: 180