Sign in with Apple REST API / Revoke tokens (JAVA)
前言
由于Apple
政策, Apple ID
登录的用户注销时需要进行Revoke Token
. 顺手记下来.
官方文档
Revoke tokens
参考文档:
Generate the Client Secret
Create a Private Key for Client Authentication
Sign in with Apple(苹果授权登陆)
代码
maven
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
工具类
package apple;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class AppleRestApiUtil {
private final String APPLE_DOMAIN = "https://appleid.apple.com";
private String client_id;
private String team_id;
private String key_id;
private String key_value;
private AppleRestApiUtil(String client_id, String team_id, String key_id, String key_value) {
this.client_id = client_id;
this.team_id = team_id;
this.key_id = key_id;
this.key_value = key_value;
}
public static AppleRestApiUtil init(String client_id, String team_id, String key_id, String key_value) {
return new AppleRestApiUtil(client_id, team_id, key_id, key_value);
}
private String getUrl(String path) {
return APPLE_DOMAIN + path;
}
/**
* 生成客户端密钥(并设置缓存时间-为了JAVA_SE都能启动去除了缓存代码)
* 用不用缓存看个人
* @return
* @throws Exception
*/
public String generateClientSecret() {
String client_secret = null;
try {
client_secret = generateClientSecret(client_id, team_id, key_id, key_value);
} catch (Exception e) {
e.printStackTrace();
}
return client_secret;
}
/**
* 参考文档: https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple#generate-the-client-secret
* 生成客户端密钥
* @param client_id 应用ID
* @param team_id 团队ID
* @param key_id 私钥ID
* @param key_value 私钥内容
* @return client_secret: 最多有效期6个月
* @throws Exception
*/
public String generateClientSecret(String client_id, String team_id, String key_id, String key_value) throws Exception{
Map<String, Object> header = new HashMap<String, Object>();
Map<String, Object> claims = new HashMap<String, Object>();
long now = new Date().getTime() / 1000;
header.put("kid", key_id); // 参考后台配置
claims.put("iss", team_id); // 参考后台配置 team id
claims.put("iat", now);
claims.put("exp", now + 60 * 60 * 24 * 30); // 最长半年,单位秒
claims.put("aud", APPLE_DOMAIN); // 默认值
claims.put("sub", client_id);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readKey(key_value));
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
String client_secret = Jwts.builder().setHeader(header).setClaims(claims).signWith(SignatureAlgorithm.ES256, privateKey).compact();
return client_secret;
}
public byte[] readKey(String key_value) throws Exception {
return Base64.decodeBase64(key_value);
}
/**
* 文档: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens
* 撤销用户token
* url: https://appleid.apple.com/auth/revoke
* @param accessToken 通过 refreshToken获取
* @return
*/
public HttpResponse revokeToken(String accessToken) {
String url = getUrl("/auth/revoke");
Map<String, Object> form = new HashMap<String, Object>();
form.put("client_id", client_id);
form.put("client_secret", generateClientSecret());
form.put("token_type_hint","access_token");
form.put("token", accessToken);
HttpResponse execute = HttpRequest.post(url).header("Content-Type", "application/x-www-form-urlencoded").form(form).execute();
return execute;
}
/**
* 文档: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
* 刷新用户 accessToken
* url: https://appleid.apple.com/auth/token
* @param refreshToken
* @return
*/
public HttpResponse refreshToken(String refreshToken) {
String url = getUrl("/auth/token");
Map<String, Object> form = new HashMap<String, Object>();
form.put("client_id", client_id);
form.put("client_secret", generateClientSecret());
form.put("grant_type","refresh_token");
form.put("refresh_token", refreshToken);
HttpResponse execute = HttpRequest.post(url).header("Content-Type", "application/x-www-form-urlencoded").form(form).execute();
return execute;
}
/**
* 文档: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
* 生成用户 accessToken
* url: https://appleid.apple.com/auth/token
* @param appleCode
* @return
*/
public HttpResponse generateToken(String appleCode) {
String url = getUrl("/auth/token");
Map<String, Object> form = new HashMap<String, Object>();
form.put("client_id", client_id);
form.put("client_secret", generateClientSecret());
form.put("grant_type","authorization_code");
form.put("code", appleCode);
HttpResponse execute = HttpRequest.post(url).header("Content-Type", "application/x-www-form-urlencoded").form(form).execute();
return execute;
}
}
测试类
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSONObject;
import org.junit.Test;
public class AppleRestApiTest {
private AppleRestApiUtil init() {
String client_id = "";
String team_id = "";
String key_id = "";
String key_value = "";
return AppleRestApiUtil.init(client_id, team_id, key_id, key_value);
}
@Test
public void testGenerateClientSecret() {
String client_secret = init().generateClientSecret();
System.out.println("client_secret: " + client_secret);
}
@Test
public void testGenerateToken() {
String appleCode = ""; // IOS端提供, 5分钟过期
HttpResponse httpResponse = init().generateToken(appleCode);
if(httpResponse.getStatus() == HttpStatus.HTTP_OK) {
String body = httpResponse.body();
JSONObject jsonObject = JSONObject.parseObject(body);
System.out.println("body: " + body);
System.out.println("access_token: " + jsonObject.getString("access_token")); // 3600s过期
System.out.println("refresh_token: " + jsonObject.getString("refresh_token"));
}
}
@Test
public void testRefreshToken() {
String refreshToken = ""; // generateToken 方法中获取
HttpResponse httpResponse = init().refreshToken(refreshToken);
if(httpResponse.getStatus() == HttpStatus.HTTP_OK) {
String body = httpResponse.body();
System.out.println("body: " + body);
}
}
@Test
public void testRevokeToken() {
String access_token = ""; // generateToken 或者 refreshToken 方法获取
HttpResponse httpResponse = init().revokeToken(access_token);
if(httpResponse.getStatus() == HttpStatus.HTTP_OK) {
String body = httpResponse.body();
System.out.println("body: " + body);
}
}
}
业务流程
- 用户登录, 通过IOS传入的code, 调用
generateToken
方法生成一个refresh_token
, 把这个值存入和用户相关的表中 - 用户注销, 因为苹果的政策需要撤销令牌, 也就是调用
revokeToken
方法
2.1 根据用户的refresh_token
调用refreshToken
方法生成一个access_token
(有效期: 3600s)
2.2 然后根据access_token
去调用revokeToken
方法 - 调用苹果
Sign in with Apple REST API
这部分的API都需要一个client_secret
, 可以每次生成也可以生成一个循环使用(最多有效期6个月)
3.1 参考文档: Generate the Client Secret
3.2 需要client_id
,team_id
,key_id
,key_value(私钥内容)
. 密钥如何生成参考: Create a Private Key for Client Authentication
后话
- 以前没使用过Apple的API. 总感觉每次都用
refresh_token
去重新生成access_token
不太好. 又没找到其他的解决方案 - 我把
client_secret
存入缓存, 如果key_value
发生改变. 就会存在client_secret
无法使用的情况. 不知道有没有办法避免这种情况. - 若文中有错误或者有更好的方案,欢迎指教
- END