网络知识 娱乐 Sign in with Apple REST API / Revoke tokens (JAVA)

Sign in with Apple REST API / Revoke tokens (JAVA)

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);
        }
    }
}

业务流程

  1. 用户登录, 通过IOS传入的code, 调用generateToken方法生成一个refresh_token, 把这个值存入和用户相关的表中
  2. 用户注销, 因为苹果的政策需要撤销令牌, 也就是调用revokeToken方法
    2.1 根据用户的refresh_token调用refreshToken方法生成一个access_token(有效期: 3600s)
    2.2 然后根据access_token去调用revokeToken方法
  3. 调用苹果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