-
앞 게시물에서 추출한 5가지의 코드로 개발을 시작해보려 한다.
# apple apple-auth-url: 'https://appleid.apple.com' apple-key-id: '@@@@@@@@@' apple-key-path-file: 'C:\work\apple\AuthKey_@@@@@@@@@.p8' apple-team-id: '@@@@@@@@@' apple-client-id: '@@@.@@@.co.kr'
[동작순서]
애플 로그인 호출(login.js) -> 애플로그인 -> 성공시 Redirect로 정보 전달
- 애플로그인 호출(login.js)
더보기/*********************************************** * * @function : login.sns.apple * * @Description : APPLE 로그인 팝업 호출 * * ***********************************************/ apple : function () { // console.log('apple login'); snsTypeInput.value = 'APPL let applePopURI = _contextPath + '/page/sns/login/apple'; const userAgent = navigator.userAgent; const isMobileYn = cyberLogin.isMobile(userAgent) ? 'Y' : 'N applePopURI += '?isMobileYn=' + isMobileY if ('Y' === isMobileYn) { location.href = applePopURI; } else { const targetName = 'Apple_Login'; const popOptions = cyberLogin.createPopupOption(applePopURI, targetName, 600, 800); const popOptStr = Object.keys(popOptions).map(function (k) { return k + '=' + popOptions[k]; }).join(',' window.open(applePopURI, targetName, popOptStr); } },
- 애플 로그인 팝업 나올 수 있게 통신 시도
더보기/** * apple <-> application 인증 요청 */ @GetMapping("/apple") public void appleLogin( HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException { String clientId = env.getAppleClientId(); // 현재 서비스 도메인을 가져옵니다. String serviceURL = ServletRequestUtil.getServiceURL(request); // 결과를 처리할 URI 정보 String redirectUri = "/page/sns/login/apple/result"; String callBackURI = serviceURL + redirectUri; // 해당 서비스 URL이 유효한 URL인지 확인 String appleCallURL = AppleApiUtil.getAppleAuthorizeURL(clientId, callBackURI); response.sendRedirect(appleCallURL); try { response.sendRedirect(appleCallURL); } catch (IOException e) { log.error("[-] appleLogin redirect error ", e); throw new Custom404Exception("애플 서비스가 준비 중입니다. 잠시 후 다시 시도해 주세요. [NCR01]"); } }
※ 만약 서비스 호출에 성공한다면 아래와 같은 팝업이 나오게 된다.
- 애플 로그인 팝업에서 로그인 성공 후 애플서비스와 통신
더보기@RequestMapping(path = { "/apple/result" }, method = { RequestMethod.GET, RequestMethod.POST }) public String appleLoginCallback( HttpServletRequest request, HttpSession session, Model model) throws UnsupportedEncodingException { log.info("[-] appleLoginCallback "); // 애플로그인 취소시 로그인페이지로 이동 if (code == null) { return "thymeleaf/common/login"; } String teamId = env.getAppleTeamId(); String clientId = env.getAppleClientId(); String keyId = env.getAppleKeyId(); String keyPath = env.getAppleKeyPathFile(); String authUrl = env.getAppleAuthUrl(); String reqUrl = authUrl + "/auth/token"; // 다른 매개변수를 필요에 따라 가져옵니다. String clientSecret = appleService.createClientSecret(teamId, clientId, keyId, keyPath, authUrl); String serviceURL = ServletRequestUtil.getServiceURL(request); // 결과를 처리할 URI 정보 String redirectUri = "/page/sns/login/apple/result"; String callBackURI = serviceURL + redirectUri; // 애플서비스를 통해 정보 요청 AppleProfileResponse profileResponse = appleService.getProfile(code, clientId, clientSecret, callBackURI); String profileErrorCode = profileResponse.getResultcode(); if ("500".equals(profileErrorCode)) { log.error("[-] Apple API Server Internal Error : {}", profileResponse.getMessage()); throw new BusinessException("Apple 서비스가 준비 중입니다. 잠시 후 다시 시도해 주세요. [NNC03]"); } if (!EcoStringUtil.isEmpty(profileResponse.getMessage())) { if (!"success".equals(profileResponse.getMessage())) { log.error("[-] Apple API Error : {}", profileResponse.getMessage()); } } log.info("[-] appleLogin get profile success "); JSONObject tokenResponse = new JSONObject(profileResponse); // 애플서비스를 통해 사용자 정보 가져오기 String appleInfo = appleService.decodeFromIdToken(tokenResponse.getString("id_token")); model.addAttribute("profile", appleInfo); return "thymeleaf/module/sns/apple/login_result"; }
- createClientSecret의 역할
- teamId, clientId, keyId, keyPath, authUrl을 통해 토큰을 생성한다.
- 이때 알고리즘을 생성할때 ES256을 사용하도록 한다.
- getProfile의 역할
- clientId, clientSecret을 param에 담아서 RestAPI 통신을 한다.
- 성공 시, JSONObject에 담아서 decodeFromIdToken 을 실행한다.
- decodeFromIdToken의 역할
- getProfile로 부터 받은 Token값을 decode 한다.
- decode된 값에서 유저의 정보를 추출하여 전달한다.
- createClientSecret
더보기/** * SERVICE : 애플 Client Secret 생성 * */ public String createClientSecret(String teamId, String clientId, String keyId, String keyPath, String authUrl) { final String strPrivateKey = readPrivateKeyBody(keyPath); PrivateKey privateKeyObj = convertToPrivateKeyObj(strPrivateKey); Map<String, Object> header = new HashMap<>(); header.put("alg", SignatureAlgorithm.ES256); header.put("kid", keyId); header.put("typ", "JWT"); final Date now = new Date(); //주의: 일반적으로 만료시간이 20분보다 크면 애플에서 거부함. https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#3878467 LocalDateTime expLdt = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault()).plusMinutes(10); Date expiredAt = java.sql.Timestamp.valueOf(expLdt); // @formatter:off String jwt = Jwts.builder() .setHeader(header) .setIssuer(teamId) .setAudience(authUrl) .setSubject(clientId) .setIssuedAt(now) //발행 시간 .setExpiration(expiredAt) //만료시간 .signWith(SignatureAlgorithm.ES256, privateKeyObj) .compact(); return jwt; } /** * private key 내용을 얻어옴 * - -----BEGIN PRIVATE KEY----- 또는 -----END PRIVATE KEY----- 와 같은 가이드라인 줄은 제외하고 실제 사용하는 부분만 파일에서 가져옴 * * @param filePath * @return */ private String readPrivateKeyBody(String filePath) { try { List<String> lines = Files.readAllLines(Paths.get(filePath)); StringBuilder sb = new StringBuilder(); for (final String line : lines) { if (!line.contains("PRIVATE KEY")) { sb.append(line); } } return sb.toString(); } catch (IOException e) { throw new RuntimeException("파일을 읽는 도중 오류가 발생했습니다.", e); } } private static PrivateKey convertToPrivateKeyObj(String strPrivateKey) { try { byte[] encodedKey = Base64.decodeBase64(strPrivateKey); return KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(encodedKey)); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new RuntimeException(e.getMessage(), e); } }
- getProfile
더보기/** * SERVICE: Apple 프로필 요청 * Apple 아이디 연동을 위한 Client 가 동의한 정보 요청 * */ public AppleProfileResponse getProfile(String code, String clientId, String clientSecret, String callbackUrl) throws UnsupportedEncodingException { Scheme scheme = Scheme.HTTPS; String host = AppleAPIHost.APPLE_OPEN_API.getHost(); String path = AppleAPIPath.GET_PROFILE.getPath(); Map<String, String> headers = new HashMap<>(); headers.put("content-type", "application/x-www-form-urlencoded"); Map<String, String> params = new HashMap<>(); params.put("client_id", clientId); params.put("client_secret", clientSecret); params.put("code", code); params.put("grant_type", "authorization_code"); String body = getParamsString(params); HttpRequestParam param = new HttpRequestParam.Builder(scheme, host, AppleAPIPath.GET_PROFILE.getMethod()) .path(path) .headers(headers) .customBody(true) .postBody(body) .debugMode(true) .build(); ResponseEntity<AppleProfileResponse> response = callAppleRestAPI(param, AppleProfileResponse.class); HttpStatus statusCode = response.getStatusCode(); log.debug("statusCode : {}", statusCode.value()); return handleResponse(response); }
- decodeFromIdToken
더보기public String decodeFromIdToken(String id_token) { try { HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest httpRequest = HttpRequest.newBuilder() .uri(URI.create("https://appleid.apple.com/auth/keys")) .GET() .build(); HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); String responseBody = response.body(); JsonParser parser = new JsonParser(); JsonObject keys = parser.parse(responseBody).getAsJsonObject(); JsonArray keyArray = keys.getAsJsonArray("keys"); // 클라이언트로부터 가져온 identity token String decode String[] decodeArray = id_token.split("\\."); String header = new String(java.util.Base64.getDecoder().decode(decodeArray[0])); // apple에서 제공해주는 kid값과 일치하는지 알기 위해 JsonElement kid = parser.parse(header).getAsJsonObject().get("kid"); JsonElement alg = parser.parse(header).getAsJsonObject().get("alg"); // 써야하는 Element (kid, alg 일치하는 element) JsonObject avaliableObject = null; for (final JsonElement element : keyArray) { JsonObject appleObject = element.getAsJsonObject(); JsonElement appleKid = appleObject.get("kid"); JsonElement appleAlg = appleObject.get("alg"); if (Objects.equals(appleKid, kid) && Objects.equals(appleAlg, alg)) { avaliableObject = appleObject; break; } } // 일치하는 공개키 없음 if (avaliableObject == null) { throw new NullPointerException("avaliableObject is null"); } PublicKey publicKey = this.getPublicKey(avaliableObject); if (publicKey == null) { throw new NullPointerException("publicKey is null"); } // 검증 Claims userInfo = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(id_token).getBody(); JsonObject userInfoObject = parser.parse(new Gson().toJson(userInfo)).getAsJsonObject(); JsonElement appleAlg = userInfoObject.get("sub"); String userId = appleAlg.getAsString(); System.out.println("userId : " + userId); return userId; } catch (IOException | InterruptedException e) { throw new BusinessException("FAILED_TO_VALIDATE_APPLE_LOGIN", e); } } private PublicKey getPublicKey(JsonObject object) { String nStr = object.get("n").toString(); String eStr = object.get("e").toString(); byte[] nBytes = Base64.decodeInteger(nStr.substring(1, nStr.length() - 1).getBytes()).toByteArray(); byte[] eBytes = Base64.decodeInteger(eStr.substring(1, eStr.length() - 1).getBytes()).toByteArray(); BigInteger n = new BigInteger(1, nBytes); BigInteger e = new BigInteger(1, eBytes); RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); KeyFactory keyFactory = null; try { keyFactory = KeyFactory.getInstance("RSA"); } catch (NoSuchAlgorithmException ex) { log.error("NoSuchAlgorithmException", ex); } if(keyFactory != null){ try { return keyFactory.generatePublic(publicKeySpec); } catch (InvalidKeySpecException ex) { log.error("InvalidKeySpecException", ex); } } return null; }
위의 과정들이 이상 없이 성공적으로 이루어졌다면, 로그인이 성공적으로 될 것이다.
감사합니다.
'개발' 카테고리의 다른 글
애플 로그인(2) (0) 2024.07.31 애플 로그인(1) (0) 2024.07.31 인텔리제이에서 스프링 레거시 프로젝트 2개 빌드 방법 (0) 2024.04.15