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
此博客主要用于记录相关知识点,大部分内容来自网络文章,特此感谢各位作者