ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 애플 로그인(3)
    개발 2024. 7. 31. 11:25

    앞 게시물에서 추출한 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
Designed by Tistory.