Revoke tokens
前言
由于苹果新政策要求在2022年6月30日之后,使用Apple ID登录的账号注销时都要调用苹果的注销Api,趁这两天事情不是特别多就把注销功能给写了,顺便记录一下。
一:实现思路
官方文档: Revoke tokens
1.1. 苹果授权登录后,会返回authorizationCode这个授权码,利用授权码调用API来生成访问令牌(access_token)和刷新令牌(refresh_token)。
access_token:调用注销Api时所需的参数
refresh_token:调用刷新Token Api时所需的参数
access_token在这个时候用不上,因为是有有效期的,我们只需要在注销的时候重新获取access_token就可以了。这个时候要将refresh_token与这个用户进行绑定,可以将其存入数据库中,为了注销的时候可以用refresh_token来获取到新的access_token
1.2.使用refresh_token来更新新的access_token
1.3.用access_token调用 Revoke tokens Api
二:代码实现
maven
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
常量类
public class Const {
//client_id(应用ID)
public static final String CLIENT_ID = "";
//teamId(苹果开发人员帐户关联的10 个字符的团队ID)
public static final String TEAM_ID = "";
//Key Id (P8证书下载后可以得到keyId,跟苹果开发人员拿)
public static final String KEY_ID = "";
//keyValue(P8文件下载后可以得到keyValue,跟苹果开发人员拿)
public static final String KEY_VALUE = "";
//AUD(固定的)
public static final String REVOKE_AUD = "https://appleid.apple.com";
//生成或刷新token的URL
public static final String AUTH_TOKEN_URL = "https://appleid.apple.com/auth/token";
//撤销用户token请求url
public static final String AUTH_REVOKE_URL = "https://appleid.apple.com/auth/revoke";
}
苹果授权登录成功后,根据authorizationCode授权码来获取refreshToken,将refreshToken与我们的用户绑定,可以存到数据库中
/**
* 根据authorizationCode授权码来获取refreshToken
*/
public String generateToken(String authorizationCode) {
String refreshToken = null;
try {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", Const.AUD);
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 = restTemplate.postForEntity(Const.AUTH_TOKEN_URL, request, String.class).getBody();
if (StringUtils.isNotBlank(value)) {
JSONObject json = JSONObject.fromObject(value);
refreshToken = json.getString("refresh_token");
}
} catch (HttpClientErrorException e) {
throw new WZHException(e.getResponseBodyAsString());//WZHException是我自定义的异常类
} catch (Exception e) {
throw new WZHException(e.getMessage());
}
return refreshToken;
}
client_secret秘钥的生成
/**
* 生成客户端密钥
*/
private static String generateClientSecret() throws Exception {
Map<String, Object> header = new HashMap<>();
header.put("kid", Const.KEY_ID); //P8文件下载得到的Key ID
header.put("alg", SignatureAlgorithm.ES256.getValue()); //SHA256
Map<String, Object> claims = new HashMap<>();
long now = new Date().getTime() / 1000;
claims.put("iss", Const.TEAM_ID);
claims.put("iat", now);
claims.put("exp", now + 648000); // 最长半年,单位秒
claims.put("aud", Const.REVOKE_AUD);
claims.put("sub", Const.CLIENT_ID);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(Const.KEY_VALUE));//P8文件下载得到的Key Value
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
return Jwts.builder().setHeader(header).setClaims(claims).signWith(SignatureAlgorithm.ES256, privateKey).compact();
}
Revoke tokens的第一种方式:使用refreshToken进行注销
public Response<String> revokeToken() {
LoginUser loginUser = getFrontUser();//从缓存获取当前登录的信息
try {
User user = userService.getById(loginUser.getId());//获取用户的refreshToken
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", Const.CLIENT_ID);
map.add("client_secret", generateClientSecret());
map.add("token_type_hint", "refresh_token");
map.add("token", user.getRefreshToken());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<String> values = restTemplate.postForEntity(Const.AUTH_REVOKE_URL, request, String.class);
log.info("注销token成功");//Apple官网上请求只有200与400,这里没有抛异常,证明是成功注销的
return Response.success("Revoke tokens success.");
} catch (HttpClientErrorException e) {
throw new WZHException(e.getResponseBodyAsString());
} catch (Exception e) {
throw new WZHException(e.getMessage());
}
}
Revoke tokens的第二种方式:使用refreshToken来刷新accessToken,再用accessToken来进行注销(个人推荐)
/**
* 根据refreshToken刷新accessToken
*/
private String refreshToken(String refreshToken) {
String accessToken = null;
try {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", Const.CLIENT_ID);
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 = restTemplate.postForEntity(Const.AUTH_TOKEN_URL, request, String.class).getBody();
if (StringUtils.isNotBlank(value)) {
JSONObject json = JSONObject.fromObject(value);
accessToken = json.getString("access_token");
}
} catch (HttpClientErrorException e) {
throw new WZHException(e.getResponseBodyAsString(), 401);
} catch (Exception e) {
throw new WZHException(e.getMessage(), 401);
}
log.info("刷新token成功");
return accessToken;
}
/**
* 撤销用户token
*/
public Response<String> revokeToken() {
LoginUser loginUser = getFrontUser();//从缓存获取当前登录的信息
try {
User user = userService.getById(loginUser.getId());
String accessToken = refreshToken(user.getRefreshToken());//更新accessToken
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", Const.CLIENT_ID);
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 = restTemplate.postForEntity(Const.AUTH_REVOKE_URL, request, String.class);
log.info("注销token成功");//Apple官网上请求只有200与400,这里没有抛异常,证明是成功注销的
return Response.success("Revoke tokens success.");
} catch (HttpClientErrorException e) {
throw new WZHException(e.getResponseBodyAsString());
} catch (Exception e) {
throw new WZHException(e.getMessage());
}
}
注意事项
-
Revoke tokens成功之后,数据库上用户绑定的refreshToken就无效了,如果再次用refreshToken去更新accessToken时,会抛
{\"error\":\"invalid_grant\",\"error_description\":\"The token has expired or has been revoked.\"}
异常,所以要根据公司具体的业务来,如果注销后可以重新登录的话,就在注销成功的时候将用户绑定的refreshToken清除掉,然后下次登录的时候重新利用authorizationCode来生成refreshToken -
revokeToken时,只要不抛异常,就证明是成功的操作,如果accessToken是错的,苹果的Api也会返回成功,因为苹果会认为这是被注销过的,所以我们的只要确保accessToken的值是没问题的就可以了
-
Apple账户修改了密码之后,refreshToken的值也会失效,所以在注销的时候,如果发现refreshToken已经失效了,得将用户绑定的refreshToken进行清除掉,同时得有个动作让用户进行重新授权来更新refreshToken。