(vue + SpingBoot)前后端分离实现Apple登录的过程
前言
????????首先介紹一下為什么寫這篇文章。最近,公司有一個(gè)項(xiàng)目,是海外的手機(jī)游戲想要上到云平臺(tái)上供各種客戶端(web,Android,ios等)可以無需下載游戲即可游玩。其中我負(fù)責(zé)web端的項(xiàng)目,項(xiàng)目需要接入第三方登錄,Google、Apple、FaceBook三種方式登錄。在我做項(xiàng)目的準(zhǔn)備工作期間,我發(fā)現(xiàn)網(wǎng)上的web端接入Apple登錄的文檔特別少,幾乎沒有......? ? ? ? 所以我打算通過官方文檔和一些“簡(jiǎn)單”的方法實(shí)現(xiàn)后發(fā)表這篇文章,以便于后續(xù)接觸到這種場(chǎng)景的人可以通過這篇文章解決你的問題!! 最后說一句:由于項(xiàng)目有點(diǎn)緊急,所以代碼的一些不規(guī)范和注釋少還請(qǐng)諒解,當(dāng)然我會(huì)盡可能說明代碼用處啥的~。
官方文檔和一些對(duì)我有用的帖子
Apple REST API 登錄文檔
JSON Web 令牌庫(kù) - jwt.io?(用于生成client_secret)
java 操作 Cookie 跨域(同頂級(jí)域名)_huaism的博客-CSDN博客
help in java_java 后端 sign in with apple 隨筆_湯一白君的博客-CSDN博客
Sign in with Apple NODE,web端接入蘋果第三方登錄 – 前端開發(fā),JQUERY特效,全棧開發(fā),vue開發(fā)
蘋果授權(quán)登錄(Sign in with Apple)-JAVA后端開發(fā)_KaisonChen的博客-CSDN博客
以上這些文檔和工具,就是我在開發(fā)過程中,所找到的一些較為有用的工具。可以提取不少信息。可能還有一些我用到的帖子,不過我給忘了,就沒貼出來了。
步驟
一、首先是需要申請(qǐng)Apple開發(fā)者賬號(hào),以及配置一些參數(shù)。
這個(gè)步驟我就不再贅述了,帖子欄最后一條鏈接里有步驟。
二、獲取一些必要的參數(shù)。
當(dāng)申請(qǐng)、配置好開發(fā)者賬號(hào)后,我們需要獲取一些必要參數(shù)。
這些是我后端所用到的所有參數(shù)。
1. KID
官方文檔的解釋:
?用于生成JWT(client_secret)的一個(gè)參數(shù),大小為10個(gè)字符串。
例如:
KID = "KOI98S78J6";2. TEAM_ID (iss)
官方文檔的解釋:
?用于生成JWT(client_secret)的一個(gè)參數(shù),大小為10個(gè)字符串。
例如:
TEAM_ID = "JI87S9KI7D";3. CLIENT_ID
即apple的應(yīng)用id,一般是域名的反寫。
例如:
CLIENT_ID = "com.example.signservice";4.?PRIVATE_KEY
代碼中拼寫錯(cuò)誤,是Private_key(私鑰),是以.p8結(jié)尾的文本文件,用作生成JWT,是作為請(qǐng)求Token時(shí)的參數(shù)之一;
改后綴為.txt
圖片打碼部分是你的KID,請(qǐng)核對(duì)好;
然后以txt文件類型打開這個(gè)文件,如下圖:
?
全部復(fù)制或者只復(fù)制中間的字符串都可,中間的字符串就是你的私鑰了。
最后一個(gè)url,是生成JWT的鏈接:https://appleid.apple.com
三、前端跳轉(zhuǎn)到apple登錄授權(quán)頁面。
這部分是由前端去完成的,拼接好鏈接調(diào)用apple的登錄授權(quán)頁面。這里我直接引用一下大佬帖子的內(nèi)容。
client_id:獲取的client_id,必傳
redirect_uri: 設(shè)置的重定向url,當(dāng)用戶同意授權(quán)后,會(huì)發(fā)起一個(gè)該URL的post請(qǐng)求,開發(fā)者需要在后臺(tái)設(shè)置相應(yīng)接口去接收他,服務(wù)端通過apple傳來的code參數(shù)去請(qǐng)求身份令牌,必傳。
scope:權(quán)限范圍,name或者email,或者兩個(gè)都設(shè)。
state:表示客戶端的當(dāng)前狀態(tài),可以指定任意值,會(huì)原封不動(dòng)地返回這個(gè)值,你可以通過它做些驗(yàn)證,防止一些攻擊。
這里面只有client_id,redirect_uri,是必須的,其他如果不設(shè)會(huì)自動(dòng)設(shè)置默認(rèn)值。
你可以使用官方提供的按鈕,當(dāng)然也可以不用,當(dāng)你點(diǎn)擊登錄按鈕后會(huì)實(shí)際會(huì)跳轉(zhuǎn)到一下地址,你可以選擇直接手動(dòng)拼接跳轉(zhuǎn)授權(quán)頁地址。
https://appleid.apple.com/auth/authorize?client_id=[CLIENT_ID]&redirect_uri=[REDIRECT_URI]&response_type=[RESPONSE_TYPE]&scope=[SCOPES]&response_mode=[RESPONSE_MODE]&state=[STATE]?
四、接收授權(quán)碼code。?
按照上面的方法拼接好鏈接,請(qǐng)求到鏈接那邊,用戶登錄apple賬號(hào)后,apple服務(wù)器將發(fā)起一個(gè)POST請(qǐng)求至當(dāng)時(shí)設(shè)置的redirect_uri,同時(shí)附上一個(gè)授權(quán)碼code,id_token用于刷新token,首次登錄將只有code和state。
然后此時(shí)需要服務(wù)端提供一個(gè)接口來接受這些參數(shù)。這時(shí)我們就需要考慮一個(gè)問題了。?
????????前端是否需要傳遞參數(shù)到后端。(注意)
????????例如,我的項(xiàng)目中需要將游戲的主鍵ID傳到后端來進(jìn)行操作,此時(shí)如果我們將接收apple服務(wù)器傳遞參數(shù)的接口和后續(xù)驗(yàn)證code和token的接口做為同一個(gè)接口,即拿到code就直接進(jìn)行驗(yàn)證的話,那前端那邊是跳轉(zhuǎn)在apple的登錄授權(quán)頁,是無法向后端傳遞游戲ID的。
????????所以我們做的這個(gè)接口只是一個(gè)中轉(zhuǎn)的接口,接收到apple傳遞的參數(shù)后需返回給前端,讓前端拿到這些參數(shù)隨帶著前端那邊需傳遞的參數(shù)一起來請(qǐng)求我們的驗(yàn)證接口。
? ? ? ? 當(dāng)然,如果前端不需要傳參數(shù)的話,就可以拿到code直接去驗(yàn)證了。
?然后我們就來寫一個(gè)中轉(zhuǎn)的接口:(注意是POST請(qǐng)求方式)
@PostMapping("/token")public void getToken(AppleIdentity appleIdentity, HttpServletResponse resp) throws IOException {Cookie tokenCookie = new Cookie("ID_TOKEN", appleIdentity.getId_token());tokenCookie.setMaxAge(100); // Cookie的存活時(shí)間(自定義)tokenCookie.setDomain("example.com");tokenCookie.setPath("/"); // 默認(rèn)路徑Cookie codeCookie = new Cookie("CODE", appleIdentity.getCode());codeCookie.setMaxAge(100); // Cookie的存活時(shí)間(自定義)codeCookie.setDomain("example.com");codeCookie.setPath("/"); // 默認(rèn)路徑resp.addCookie(tokenCookie);resp.addCookie(codeCookie);resp.sendRedirect("https://xxxxx.example.com/game/play");}這里我是做的重定向去跳轉(zhuǎn)回前端的頁面,這里傳參的話有兩種方式:
? ? ? ? 1. 第一種(不推薦):直接在前端鏈接上拼接參數(shù)
https://xxxxx.example.com/game/play?code=xxxxxx&id_token=xxxxxxx
? ? ? ? 2. 第二種:存入cookie。
?java 操作 Cookie 跨域(同頂級(jí)域名)_huaism的博客-CSDN博客
👆👆👆這個(gè)帖子介紹了跨域操作cookie,我這里不多解釋了。
最后,提醒一下之前配置apple時(shí),重定向的url配置需要配置到這個(gè)中轉(zhuǎn)的接口
?這里是引用了別人的圖片,配置位置給大家指出來了,填上你的中轉(zhuǎn)接口即可。
四、使用code等參數(shù)請(qǐng)求id_token
將code返回給前端后,前端將code和其他需要傳的參數(shù)來請(qǐng)求我們的驗(yàn)證接口,驗(yàn)證接口的第一步就是獲取到token:
以下是返回參數(shù)的字段:
{
? ? "access_token": "adg61.######.670r9",
? ? "token_type": "Bearer",
? ? "expires_in": 3600,
? ? "refresh_token": "rca7.######.IABoQ",
? ? "id_token": "eyJra.######..96sZg"
}
填充參數(shù),發(fā)送post請(qǐng)求,得到JSON串,解析JSON,得到id_token.?
Map<String, Object> paramsForm = new HashMap();paramsForm.put("grant_type", "authorization_code");paramsForm.put("code", appleIdentity.getCode());paramsForm.put("client_id", CLIENT_ID);paramsForm.put("client_secret", generateClientSecret(KID, TEAM_ID, CLIENT_ID, PRIMARY_KEY));Map<String, Object> headers = new HashMap<>();headers.put(Header.CONTENT_TYPE, "application/x-www-form-urlencoded");HttpKits.buildGetUrl("https://appleid.apple.com/auth/token", paramsForm);String tokenResult = null;try {tokenResult = HttpKits.post("https://appleid.apple.com/auth/token", headers, paramsForm);} catch (Exception e) {e.printStackTrace();}logger.info("[auth_token]<result> ======> " + tokenResult);if (StringUtils.isBlank(tokenResult)) {throw new UtilException("Request Token Failed!");}JSONObject jsonObject = JSONObject.parseObject(tokenResult);String idToken = jsonObject.getString("id_token");if (idToken == null) {throw new UtilException("ID TOKEN ERROR!");}其中
generateClientSecret(KID, TEAM_ID, CLIENT_ID, PRIMARY_KEY))這個(gè)方法就是用于生成JWT的方法。👇👇👇下面帖子有介紹,不過可能不太方便看。
help in java_java 后端 sign in with apple 隨筆_湯一白君的博客-CSDN博客
我這邊重新貼一下代碼,其中的參數(shù)前面已經(jīng)讓大家獲取到了。
/*** 生成clientSecret** @param kid* @param teamId* @param clientId* @param primaryKey(寫完發(fā)現(xiàn),命名有誤,privateKey)* @return*/public String generateClientSecret(String kid, String teamId,String clientId, String primaryKey) {Map header = new HashMap<>();header.put("kid", kid);long second = System.currentTimeMillis() / 1000;//將private key字符串轉(zhuǎn)換成PrivateKey 對(duì)象PrivateKey privateKey = null;try {PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readPrimaryKey(primaryKey));KeyFactory keyFactory = KeyFactory.getInstance("EC");privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);} catch (Exception e) {e.printStackTrace();}// 此處只需PrimaryKeyAlgorithm algorithm = Algorithm.ECDSA256(null,(ECPrivateKey) privateKey);// 生成JWT格式的client_secretString secret = JWT.create().withHeader(header).withClaim("iss", teamId).withClaim("iat", second).withClaim("exp", 86400 * 180 + second).withClaim("aud", APPLE_JWT_AUD_URL).withClaim("sub", clientId).sign(algorithm);return secret;}private byte[] readPrimaryKey(String primaryKey) {StringBuilder pkcs8Lines = new StringBuilder();BufferedReader rdr = new BufferedReader(new StringReader(primaryKey));String line = "";try {while ((line = rdr.readLine()) != null) {pkcs8Lines.append(line);}} catch (IOException e) {e.printStackTrace();}// 需要注意刪除 "BEGIN" and "END" 行, 以及空格String pkcs8Pem = pkcs8Lines.toString();pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "");pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "");pkcs8Pem = pkcs8Pem.replaceAll("\\s+", "");// Base64 轉(zhuǎn)碼return Base64.decodeBase64(pkcs8Pem);}上面代碼中的Algorithm可能無法引入,需要去JWT令牌庫(kù)中找到Java的Maven添加依賴項(xiàng)
👇👇👇下面就是JWT令牌庫(kù)了。
JSON Web Token Libraries - jwt.io
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.2.1</version></dependency>👆👆👆上面是我用的Maven依賴,大家可以自行選擇。
五、id_token驗(yàn)證
獲取到id_token之后就是驗(yàn)證id_token了
驗(yàn)證的方法我是用的這個(gè)帖子的方法👇👇👇
蘋果授權(quán)登錄(Sign in with Apple)-JAVA后端開發(fā)
驗(yàn)證后解析此token可獲取到apple用戶的信息,返回的數(shù)據(jù)格式如下:
{"at_hash": "CMq1E######Ai8zyw","aud": "com.######.######service","sub": "000724.######8175f70.0554","nonce_supported": true,"auth_time": 1668775722,"iss": "https://appleid.apple.com","exp": 1668862128,"iat": 1668775728 }我的代碼如下:
try {Map<String, String> map = new HashMap<String, String>();//驗(yàn)證identityTokenif (!verify(idToken)) {throw new UtilException("JSON Validated Failed!");}//對(duì)identityToken解碼logger.info("<token> ===> " + idToken);JSONObject json = parserIdentityToken(idToken);if (json == null) {throw new UtilException("JSON Decoded Failed!");}String userId = json.getString("sub"); //下面是我項(xiàng)目里的業(yè)務(wù)邏輯了(換成你們處理拿到的用戶信息的業(yè)務(wù)邏輯就行了)MGame mGame = mGameService.getById(appleIdentity.getGameId());Map<String, Object> form = new HashMap();form.put("uid", userId);form.put("game_id", appleIdentity.getGameId());form.put("platform_id", appleIdentity.getPlatformId());form.put("token", idToken);form.put("channel_code", "apple");form.put("os_type", appleIdentity.getOsType());HttpKits.buildGetUrl("xxxxx", form);String result = null;try {result = HttpKits.post("xxxxx", form);} catch (Exception e) {e.printStackTrace();}if (StringUtils.isBlank(result)) {throw new UtilException("Login Failed!");}JSONObject tokenJson = JSONObject.parseObject(result);String data = tokenJson.getString("data");String code = tokenJson.getString("code");if (!code.equals("200")) {throw new UtilException("Decoded Failed!");}JSONObject dataJsonObject = JSONObject.parseObject(data);String super_user_id = dataJsonObject.getString("super_user_id");String token = dataJsonObject.getString("token");map.put("userId", userId);map.put("screenType", mGame.getScreenType().toString());map.put("superUserId", super_user_id);map.put("token", token);map.put("info", json.toJSONString());return AjaxResult.success("data", map);} catch (Exception e) {logger.error("app login error:" + e.getMessage(), e);throw new UtilException(e);} private static JSONArray getAuthKeys() throws Exception {String url = "https://appleid.apple.com/auth/keys";RestTemplate restTemplate = new RestTemplate();JSONObject json = restTemplate.getForObject(url, JSONObject.class);JSONArray arr = json.getJSONArray("keys");return arr;}public static Boolean verify(String jwt) throws Exception {JSONArray arr = getAuthKeys();if (arr == null) {return false;}JSONObject authKey = null;//先取蘋果第一個(gè)key進(jìn)行校驗(yàn)authKey = arr.getJSONObject(0);if (verifyExc(jwt, authKey)) {return true;} else {//再取第二個(gè)key校驗(yàn)authKey = arr.getJSONObject(1);if (verifyExc(jwt, authKey)) {return true;} else {//再取第三個(gè)key校驗(yàn)authKey = arr.getJSONObject(2);return verifyExc(jwt, authKey);}}}/*** 對(duì)前端傳來的identityToken進(jìn)行驗(yàn)證** @param jwt 對(duì)應(yīng)前端傳來的 identityToken* @param authKey 蘋果的公鑰 authKey* @return* @throws Exception*/public static Boolean verifyExc(String jwt, JSONObject authKey) throws Exception {Jwk jwa = Jwk.fromValues(authKey);PublicKey publicKey = jwa.getPublicKey();String aud = "";String sub = "";if (jwt.split("\\.").length > 1) {String claim = new String(Base64.decodeBase64(jwt.split("\\.")[1]));aud = JSONObject.parseObject(claim).get("aud").toString();sub = JSONObject.parseObject(claim).get("sub").toString();}JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);jwtParser.requireIssuer("https://appleid.apple.com");jwtParser.requireAudience(aud);jwtParser.requireSubject(sub);try {Jws<Claims> claim = jwtParser.parseClaimsJws(jwt);if (claim != null && claim.getBody().containsKey("auth_time")) {System.out.println(claim);return true;}return false;} catch (ExpiredJwtException e) {logger.error("apple identityToken expired", e);return false;} catch (Exception e) {logger.error("apple identityToken illegal", e);return false;}}/*** 對(duì)前端傳來的JWT字符串identityToken的第二部分進(jìn)行解碼* 主要獲取其中的aud和sub,aud大概對(duì)應(yīng)ios前端的包名,sub大概對(duì)應(yīng)當(dāng)前用戶的授權(quán)的openID** @param identityToken* @return {"aud":"com.xkj.****","sub":"000***.8da764d3f9e34d2183e8da08a1057***.0***","c_hash":"UsKAuEoI-****","email_verified":"true","auth_time":1574673481,"iss":"https://appleid.apple.com","exp":1574674081,"iat":1574673481,"email":"****@qq.com"}*/public static JSONObject parserIdentityToken(String identityToken) {String[] arr = identityToken.split("\\.");Base64 base64 = new Base64();String decode = new String(base64.decodeBase64(arr[1]));String substring = decode.substring(0, decode.indexOf("}") + 1);JSONObject jsonObject = JSON.parseObject(substring);return jsonObject;}其中應(yīng)該要注意的點(diǎn)是,原帖子中只取了兩個(gè)公鑰public_key去驗(yàn)證,會(huì)有出現(xiàn)驗(yàn)證失敗的情況,是因?yàn)?https://appleid.apple.com/auth/keys 這個(gè)鏈接的公鑰實(shí)際是存在三個(gè)的(我請(qǐng)求的是三個(gè),具體幾個(gè)需要自行請(qǐng)求一下查看有多少個(gè)公鑰),需要三個(gè)公鑰全部去驗(yàn)證一下。
下面是我請(qǐng)求公鑰鏈接的截圖?
到這里,apple登錄整個(gè)流程就結(jié)束了, 流程看起來簡(jiǎn)單,但是實(shí)現(xiàn)起來處處碰壁踩坑,,,其中非常不爽的一點(diǎn)是接口測(cè)試必須在線上測(cè)試,所以代碼中很多的logger.info()方法去打印結(jié)果就是因?yàn)榫€上無法獲取準(zhǔn)確的報(bào)錯(cuò)點(diǎn),所以我就每個(gè)結(jié)果去打印測(cè)試。代碼中應(yīng)該還有很多不足的地方,歡迎大家踴躍地發(fā)表意見,期盼大佬們的指點(diǎn)~
以下是我的完整的代碼,有需要的自取哈:
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.auth0.jwk.Jwk; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.qpyx.common.annotation.SecretBody; import com.qpyx.common.core.AjaxResult; import com.qpyx.common.core.entity.AppleIdentity; import com.qpyx.common.utils.HttpKits; import com.qpyx.common.utils.StringUtils; import com.qpyx.common.utils.exception.UtilException; import com.qpyx.game.entity.MGame; import com.qpyx.game.service.MGameService; import io.jsonwebtoken.*; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate;import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.ECPrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.HashMap; import java.util.Map;@RestController @RequestMapping("/apple") public class AppleLoginController {@Autowiredprivate MGameService mGameService;private static final Logger logger = LoggerFactory.getLogger(AppleLoginController.class);public static final String KID = "xx";public static final String TEAM_ID = "xx";public static final String CLIENT_ID = "xx";public static final String PRIMARY_KEY = "xx";public static final String APPLE_JWT_AUD_URL = "https://appleid.apple.com";@PostMapping("/token")public void getToken(AppleIdentity appleIdentity, HttpServletResponse resp) throws IOException {Cookie tokenCookie = new Cookie("ID_TOKEN", appleIdentity.getId_token());tokenCookie.setMaxAge(100); // Cookie的存活時(shí)間(自定義)tokenCookie.setDomain("xx.com");tokenCookie.setPath("/"); // 默認(rèn)路徑Cookie codeCookie = new Cookie("CODE", appleIdentity.getCode());codeCookie.setMaxAge(100); // Cookie的存活時(shí)間(自定義)codeCookie.setDomain("xx.com");codeCookie.setPath("/"); // 默認(rèn)路徑resp.addCookie(tokenCookie);resp.addCookie(codeCookie);resp.sendRedirect("https://xx.com/game/play");}@SecretBody@PostMapping("/verify")public AjaxResult appleLogin(@RequestBody AppleIdentity appleIdentity) {Map<String, Object> paramsForm = new HashMap();paramsForm.put("grant_type", "authorization_code");paramsForm.put("code", appleIdentity.getCode());paramsForm.put("client_id", CLIENT_ID);paramsForm.put("client_secret", generateClientSecret(KID, TEAM_ID, CLIENT_ID, PRIMARY_KEY));Map<String, Object> headers = new HashMap<>();headers.put(Header.CONTENT_TYPE, "application/x-www-form-urlencoded");HttpKits.buildGetUrl("https://appleid.apple.com/auth/token", paramsForm);String tokenResult = null;try {tokenResult = HttpKits.post("https://appleid.apple.com/auth/token", headers, paramsForm);} catch (Exception e) {e.printStackTrace();}logger.info("[auth_token]<result> ======> " + tokenResult);if (StringUtils.isBlank(tokenResult)) {throw new UtilException("Request Token Failed!");}JSONObject jsonObject = JSONObject.parseObject(tokenResult);String idToken = jsonObject.getString("id_token");if (idToken == null) {throw new UtilException("ID TOKEN ERROR!");}try {Map<String, String> map = new HashMap<String, String>();//驗(yàn)證identityTokenif (!verify(idToken)) {throw new UtilException("JSON Validated Failed!");}//對(duì)identityToken解碼logger.info("<token> ===> " + idToken);JSONObject json = parserIdentityToken(idToken);logger.info("<AppleUserInfoJson> ====>" + json.toString());if (json == null) {throw new UtilException("JSON Decoded Failed!");}String userId = json.getString("sub");MGame mGame = mGameService.getById(appleIdentity.getGameId());Map<String, Object> form = new HashMap();form.put("uid", userId);form.put("game_id", appleIdentity.getGameId());form.put("platform_id", appleIdentity.getPlatformId());form.put("token", idToken);form.put("channel_code", "apple");form.put("os_type", appleIdentity.getOsType());HttpKits.buildGetUrl("xx", form);String result = null;try {result = HttpKits.post("xx", form);} catch (Exception e) {e.printStackTrace();}if (StringUtils.isBlank(result)) {throw new UtilException("Login Failed!");}JSONObject tokenJson = JSONObject.parseObject(result);String data = tokenJson.getString("data");String code = tokenJson.getString("code");if (!code.equals("200")) {throw new UtilException("Decoded Failed!");}JSONObject dataJsonObject = JSONObject.parseObject(data);String super_user_id = dataJsonObject.getString("super_user_id");String token = dataJsonObject.getString("token");map.put("userId", userId);map.put("screenType", mGame.getScreenType().toString());map.put("superUserId", super_user_id);map.put("token", token);map.put("info", json.toJSONString());return AjaxResult.success("data", map);} catch (Exception e) {logger.error("app login error:" + e.getMessage(), e);throw new UtilException(e);}}/*** 生成clientSecret** @param kid* @param teamId* @param clientId* @param primaryKey(寫完發(fā)現(xiàn),命名有誤,privateKey)* @return*/public String generateClientSecret(String kid, String teamId,String clientId, String primaryKey) {Map header = new HashMap<>();header.put("kid", kid);long second = System.currentTimeMillis() / 1000;//將private key字符串轉(zhuǎn)換成PrivateKey 對(duì)象PrivateKey privateKey = null;try {PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readPrimaryKey(primaryKey));KeyFactory keyFactory = KeyFactory.getInstance("EC");privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);} catch (Exception e) {e.printStackTrace();}// 此處只需PrimaryKeyAlgorithm algorithm = Algorithm.ECDSA256(null,(ECPrivateKey) privateKey);// 生成JWT格式的client_secretString secret = JWT.create().withHeader(header).withClaim("iss", teamId).withClaim("iat", second).withClaim("exp", 86400 * 180 + second).withClaim("aud", APPLE_JWT_AUD_URL).withClaim("sub", clientId).sign(algorithm);return secret;}private byte[] readPrimaryKey(String primaryKey) {StringBuilder pkcs8Lines = new StringBuilder();BufferedReader rdr = new BufferedReader(new StringReader(primaryKey));String line = "";try {while ((line = rdr.readLine()) != null) {pkcs8Lines.append(line);}} catch (IOException e) {e.printStackTrace();}// 需要注意刪除 "BEGIN" and "END" 行, 以及空格String pkcs8Pem = pkcs8Lines.toString();pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "");pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "");pkcs8Pem = pkcs8Pem.replaceAll("\\s+", "");// Base64 轉(zhuǎn)碼return Base64.decodeBase64(pkcs8Pem);}private static JSONArray getAuthKeys() throws Exception {String url = "https://appleid.apple.com/auth/keys";RestTemplate restTemplate = new RestTemplate();JSONObject json = restTemplate.getForObject(url, JSONObject.class);JSONArray arr = json.getJSONArray("keys");return arr;}public static Boolean verify(String jwt) throws Exception {JSONArray arr = getAuthKeys();if (arr == null) {return false;}JSONObject authKey = null;//先取蘋果第一個(gè)key進(jìn)行校驗(yàn)authKey = arr.getJSONObject(0);if (verifyExc(jwt, authKey)) {return true;} else {//再取第二個(gè)key校驗(yàn)authKey = arr.getJSONObject(1);if (verifyExc(jwt, authKey)) {return true;} else {authKey = arr.getJSONObject(2);return verifyExc(jwt, authKey);}}}/*** 對(duì)前端傳來的identityToken進(jìn)行驗(yàn)證** @param jwt 對(duì)應(yīng)前端傳來的 identityToken* @param authKey 蘋果的公鑰 authKey* @return* @throws Exception*/public static Boolean verifyExc(String jwt, JSONObject authKey) throws Exception {Jwk jwa = Jwk.fromValues(authKey);PublicKey publicKey = jwa.getPublicKey();String aud = "";String sub = "";if (jwt.split("\\.").length > 1) {String claim = new String(Base64.decodeBase64(jwt.split("\\.")[1]));aud = JSONObject.parseObject(claim).get("aud").toString();sub = JSONObject.parseObject(claim).get("sub").toString();}JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);jwtParser.requireIssuer("https://appleid.apple.com");jwtParser.requireAudience(aud);jwtParser.requireSubject(sub);try {Jws<Claims> claim = jwtParser.parseClaimsJws(jwt);if (claim != null && claim.getBody().containsKey("auth_time")) {System.out.println(claim);return true;}return false;} catch (ExpiredJwtException e) {logger.error("apple identityToken expired", e);return false;} catch (Exception e) {logger.error("apple identityToken illegal", e);return false;}}/*** 對(duì)前端傳來的JWT字符串identityToken的第二部分進(jìn)行解碼* 主要獲取其中的aud和sub,aud大概對(duì)應(yīng)ios前端的包名,sub大概對(duì)應(yīng)當(dāng)前用戶的授權(quán)的openID** @param identityToken* @return {"aud":"com.xkj.****","sub":"000***.8da764d3f9e34d2183e8da08a1057***.0***","c_hash":"UsKAuEoI-****","email_verified":"true","auth_time":1574673481,"iss":"https://appleid.apple.com","exp":1574674081,"iat":1574673481,"email":"****@qq.com"}*/public static JSONObject parserIdentityToken(String identityToken) {String[] arr = identityToken.split("\\.");Base64 base64 = new Base64();String decode = new String(base64.decodeBase64(arr[1]));String substring = decode.substring(0, decode.indexOf("}") + 1);JSONObject jsonObject = JSON.parseObject(substring);return jsonObject;} }總結(jié)
以上是生活随笔為你收集整理的(vue + SpingBoot)前后端分离实现Apple登录的过程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中兴zxr10路由器重启命令_中兴ZXR
- 下一篇: Vue.js使用流程