关于Sign in With Apple (苹果授权登录)的问题,公司app上架appStore被拒原因是使用第三方授权登陆但是却没有使用苹果账号的授权登陆,苹果强制要求涉及到第三方登陆的app要集成apple账号登录,成功后特此记录一下。
一、第一步要引入的jar包
<!-- ↓↓↓↓↓↓↓↓ Sign in With Apple (苹果APP授权登录)相关↓↓↓↓↓↓↓↓ zh-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<!-- ↑↑↑↑↑↑↑↑ Sign in With Apple (苹果APP授权登录)相关 ↑↑↑↑↑↑↑↑ zh-->
二、第二步加上网络请求工具类 HttpClientUtils
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* HttpClient工具类
* @author zhanghao
*/
public class HttpClientUtils
{
private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); // 日志记录
private static RequestConfig requestConfig = null;
static
{
// 设置请求和传输超时时间
requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build();
}
/**
* post请求传输json参数
* @param url url地址
* @param json 参数
* @return
*/
public static JSONObject httpPost(String url, JSONObject jsonParam)
{
// post请求返回结果
CloseableHttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost httpPost = new HttpPost(url);
// 设置请求和传输超时时间
httpPost.setConfig(requestConfig);
try
{
if (null != jsonParam)
{
// 解决中文乱码问题
StringEntity entity = new StringEntity(jsonParam.toString(), "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
CloseableHttpResponse result = httpClient.execute(httpPost);
// 请求发送成功,并得到响应
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
String str = "";
try
{
// 读取服务器返回过来的json字符串数据
str = EntityUtils.toString(result.getEntity(), "utf-8");
// 把json字符串转换成json对象
jsonResult = JSONObject.parseObject(str);
}
catch (Exception e)
{
logger.error("post请求提交失败:" + url, e);
}
}
}
catch (IOException e)
{
logger.error("post请求提交失败:" + url, e);
}
finally
{
httpPost.releaseConnection();
}
return jsonResult;
}
/**
* post请求传输String参数 例如:name=Jack&sex=1&type=2
* Content-type:application/x-www-form-urlencoded
* @param url url地址
* @param strParam 参数
* @return
*/
public static JSONObject httpPost(String url, String strParam)
{
// post请求返回结果
CloseableHttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
try
{
if (null != strParam)
{
// 解决中文乱码问题
StringEntity entity = new StringEntity(strParam, "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/x-www-form-urlencoded");
httpPost.setEntity(entity);
}
CloseableHttpResponse result = httpClient.execute(httpPost);
// 请求发送成功,并得到响应
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
String str = "";
try
{
// 读取服务器返回过来的json字符串数据
str = EntityUtils.toString(result.getEntity(), "utf-8");
// 把json字符串转换成json对象
jsonResult = JSONObject.parseObject(str);
}
catch (Exception e)
{
logger.error("post请求提交失败:" + url, e);
}
}
}
catch (IOException e)
{
logger.error("post请求提交失败:" + url, e);
}
finally
{
httpPost.releaseConnection();
}
return jsonResult;
}
/**
* 发送get请求
* @param url 路径
* @return
*/
public static JSONObject httpGet(String url)
{
// get请求返回结果
JSONObject jsonResult = null;
CloseableHttpClient client = HttpClients.createDefault();
// 发送get请求
HttpGet request = new HttpGet(url);
request.setConfig(requestConfig);
try
{
CloseableHttpResponse response = client.execute(request);
// 请求发送成功,并得到响应
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
// 读取服务器返回过来的json字符串数据
HttpEntity entity = response.getEntity();
String strResult = EntityUtils.toString(entity, "utf-8");
// 把json字符串转换成json对象
jsonResult = JSONObject.parseObject(strResult);
}
else
{
logger.error("get请求提交失败:" + url);
}
}
catch (IOException e)
{
logger.error("get请求提交失败:" + url, e);
}
finally
{
request.releaseConnection();
}
return jsonResult;
}
}
三、第三步加上与苹果服务器交互和验证SignInWithApple
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import java.security.PublicKey;
import org.apache.commons.codec.binary.Base64;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwk.Jwk;
/**
*
* @author zhanghao
*
*/
public class SignInWithApple {
/**
* 解密信息
*
* @param identityToken APP获取的identityToken
* @return 解密参数:失败返回null
*/
public static String verify(String identityToken) {
try {
if (identityToken.split("\\.").length > 1) {
String claim = new String(Base64.decodeBase64(identityToken.split("\\.")[1]));
String aud = JSONObject.parseObject(claim).get("aud").toString();
String sub = JSONObject.parseObject(claim).get("sub").toString();
String reuslt ="FAIL";
//此处循环遍历的原因是因为只取第一个会出现非法token的异常
for (int i = 0; i < getPublicKey().size(); i++) {
JSONObject jsonObject1 = JSONObject.parseObject(getPublicKey().getString(i));
Jwk jwa = Jwk.fromValues(jsonObject1);
PublicKey publicKey = jwa.getPublicKey();
reuslt = verify(publicKey, identityToken, aud, sub);
if(reuslt.equals("SUCCESS")){
break;
}
}
if (reuslt.equals("SUCCESS")) {
System.out.println(reuslt);
return claim;
}else{
System.out.println(reuslt);
return "FAIL";
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 此处验证
* @param key
* @param jwt
* @param audience
* @param subject
* @return
* @throws Exception
*/
public static String verify(PublicKey key, String jwt, String audience, String subject) throws Exception {
String result = "FAIL";
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")) {
result = "SUCCESS";
return result;
}
} catch (ExpiredJwtException e) {
result = "苹果token过期";
return result;
} catch (SignatureException e) {
result = "苹果token非法";
return result;
}
return result;
}
/**
* 生成公钥串
*
* @return 构造好的公钥串
*/
public static JSONArray getPublicKey() {
try {
String str = HttpClientUtils.httpGet("https://appleid.apple.com/auth/keys").toString();
JSONObject data = JSONObject.parseObject(str);
String keys = data.getString("keys");
JSONArray arr = JSONObject.parseArray(keys);
return arr;
} catch (final Exception e) {
e.printStackTrace();
}
return null;
}
}
四、第四步处理自己的业务逻辑
暴露给移动端的接口
参数:
String identityToken, 拉取信息
String authorizationCode, 苹果说要传的但是没用到
String fullName, 用户名
String email, 邮箱
String appleId 唯一id 也就是UserId 用于与自己业务系统账号绑定
// 请求接口获取返回值
String is = SignInWithApple.verify(identityToken);
//验证成功后
if (is!="FAIL") {
// 请求成功
// 根据appleId查詢系统中是否有該用戶
此处省略查询用户的方法
if(false){
//如果没有该用户调用创建用户方法
此处省略创建用户的方法
}else{
//如果有该用户更新token
此处省略更新token的方法
}
}else {
//验证失败
}