본문 바로가기
Spring

[Spring] REST 방식으로 애플 로그인 구현하기 - 2

by 태진아밴드 2021. 7. 14.

글이 너무 길어지는거 같아 2부로 나눴다.

 

1부에서는 애플 개발자계정에서 설정할 수 있는 설정 및 키 파일을 다운로드 받았다.

 

이제 프로젝트에 적용해보자.

 

우선 개발자 계정에서 설정한 정보들을 properties 파일에 담아주자.

 

이전에 어떤 분께서 properties 설정을 어떻게 했는지 물어보셨는데 블로그를 안본지 몇달이 지난뒤라 이제서야 답변을 달아드렸다 😅

 

그래서 이번엔 프로퍼티도 같이 첨부하며 진행하려 한다. 나같은 경우에는 이런식으로 설정했다.

 

config.properties

# apple login 관련
apple.auth.url=https://appleid.apple.com
apple.team.id=본인의 Team ID
apple.redirect.url=본인이 설정한 redirect url
apple.client.id=본인의 Identifier
apple.key.id=본인의 Key id
apple.key.path=Key file 경로

 

이제 예전과 마찬가지로 로그인 창을 호출하는 부분을 만들어보자.

 

* Java

// 애플 로그인 호출
@RequestMapping(value = "/getAppleAuthUrl")
public @ResponseBody String getAppleAuthUrl(HttpServletRequest request) throws Exception {

    String reqUrl = appleAuthUrl + "/auth/authorize?client_id=" + client_id + "&redirect_uri=" + redirect_uri
            + "&response_type=code id_token&response_mode=form_post";

    return reqUrl;
}

* Javasript

// 애플 로그인 버튼 클릭
function loginWithApple() {
    $.ajax({
        url: '/login/getAppleAuthUrl',
        type: 'get',
    }).done(function (res) {
        location.href = res;
    });
}

 

이전과 마찬가지로 스크립트단에서 ajax로 호출하면 해당 페이지로 리턴시키는 간단한 형태이다.

 

해당 페이지로 리턴되게 되면 기타 sns 로그인들과 마찬가지로 애플 아이디로 로그인하는 화면이 뜨게되고

 

로그인 이후에는 개발자 계정에서 설정했던 reditect url로 사용자 정보를 JSON으로 보내주며 리턴되게 된다.

 

애플로그인 관련된 처리는 AppleLoginUtil로 만들어 처리하였는데

 

우선 AppleLoginUtil 소스를 추가 하기전 JWT 처리를 위해 pom.xml에 의존성을 하나 추가해주자

 

* pom.xml

<!-- jwt -->
<dependency>
	<groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>3.10</version>
</dependency>

 

* AppleLoginUtil

// 애플 로그인 util
public class AppleLoginUtil {

    private static final Logger logger = LoggerFactory.getLogger(AppleLoginUtil.class);
    private static ObjectMapper objectMapper = new ObjectMapper();

    public String createClientSecret(String teamId, String clientId, String keyId, String keyPath, String authUrl) {

        JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(keyId).build();
        JWTClaimsSet claimsSet = new JWTClaimsSet();
        Date now = new Date();

        claimsSet.setIssuer(teamId);
        claimsSet.setIssueTime(now);
        claimsSet.setExpirationTime(new Date(now.getTime() + 3600000));
        claimsSet.setAudience(authUrl);
        claimsSet.setSubject(clientId);

        SignedJWT jwt = new SignedJWT(header, claimsSet);

        try {
            ECPrivateKey ecPrivateKey = new ECPrivateKeyImpl(readPrivateKey(keyPath));
            JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS());

            jwt.sign(jwsSigner);

        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        }

        return jwt.serialize();
    }


    private byte[] readPrivateKey(String keyPath) {

        Resource resource = new ClassPathResource(keyPath);
        byte[] content = null;

        try (FileReader keyReader = new FileReader(resource.getFile());
             PemReader pemReader = new PemReader(keyReader)) {
            {
                PemObject pemObject = pemReader.readPemObject();
                content = pemObject.getContent();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return content;
    }

    public String doPost(String url, Map<String, String> param) {
        String result = null;
        CloseableHttpClient httpclient = null;
        CloseableHttpResponse response = null;
        Integer statusCode = null;
        String reasonPhrase = null;
        try {
            httpclient = HttpClients.createDefault();
            HttpPost httpPost = new HttpPost(url);
            httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
            List<NameValuePair> nvps = new ArrayList<>();
            Set<Map.Entry<String, String>> entrySet = param.entrySet();
            for (Map.Entry<String, String> entry : entrySet) {
                String fieldName = entry.getKey();
                String fieldValue = entry.getValue();
                nvps.add(new BasicNameValuePair(fieldName, fieldValue));
            }
            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nvps);
            httpPost.setEntity(formEntity);
            response = httpclient.execute(httpPost);
            statusCode = response.getStatusLine().getStatusCode();
            reasonPhrase = response.getStatusLine().getReasonPhrase();
            HttpEntity entity = response.getEntity();
            result = EntityUtils.toString(entity, "UTF-8");

            if (statusCode != 200) {
                logger.error("[error] : " + result);
            }
            EntityUtils.consume(entity);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                if (httpclient != null) {
                    httpclient.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    public JSONObject decodeFromIdToken(String id_token) {

        try {
            SignedJWT signedJWT = SignedJWT.parse(id_token);
            ReadOnlyJWTClaimsSet getPayload = signedJWT.getJWTClaimsSet();
            ObjectMapper objectMapper = new ObjectMapper();
            JSONObject payload = objectMapper.readValue(getPayload.toJSONObject().toJSONString(), JSONObject.class);

            if (payload != null) {
                return payload;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

}

보는바와 같이 createClientSecret 을 호출할때에는 설정한 키 파일 경로에서 파일을 읽어오기 때문에

 

해당 경로에 키 파일이 있어야 한다. 

 

이제 컨트롤러단 소스를 보자.

 

* JAVA

// 애플 연동정보 조회
@RequestMapping(value = "/oauth_apple")
public String oauth_apple(HttpServletRequest request, @RequestParam(value = "code") String code, HttpServletResponse response) throws Exception {

    String client_id = appleClentId;
    String client_secret = AppleLoginUtil.createClientSecret(appleTeamId, appleClentId, appleKeyId, appleKeyPath, applehUrl);

    String reqUrl = appleAuthUrl + "/auth/token";

    Map<String, String> tokenRequest = new HashMap<>();

    tokenRequest.put("client_id", client_id);
    tokenRequest.put("client_secret", client_secret);
    tokenRequest.put("code", code);
    tokenRequest.put("grant_type", "authorization_code");

    String apiResponse = AppleLoginUtil.doPost(reqUrl, tokenRequest);

    ObjectMapper objectMapper = new ObjectMapper();
    JSONObject tokenResponse = objectMapper.readValue(apiResponse, JSONObject.class);

    // 애플 정보조회 성공
    if (tokenResponse.get("error") == null ) {

        JSONObject payload = AppleLoginUtil.decodeFromIdToken(tokenResponse.getString("id_token"));
        //  회원 고유 식별자
        String appleUniqueNo = payload.getString("sub");
        
        /** 
                
            TO DO : 리턴받은 appleUniqueNo 해당하는 회원정보 조회 후 로그인 처리 후 메인으로 이동
                
         */


    // 애플 정보조회 실패
    } else {
        throw new ErrorMessage("애플 정보조회에 실패했습니다.");
    }

}

컨트롤러 단에서는 AppleLoginUtil 의 createClientSecret를 통해 토큰 호출에 필요한 client_secret을 추출하게 되고

 

토큰 호출에 성공했을경우 AppleLoginUtil의 decodeFromIdToken을 통해 회원 고유 식별자를 호출하게 된다.

 

회원고유 식별자로 기존과 같이 로그인 로직을 처리해주면 애플로그인도 연동 완료!

 

남은 건 구글 로그인인데 다음번에 포스팅하기로 하자. 

 

( 이전에도 다음번에 구글로그인 하기로 했는데 애플로그인을 먼저 한건 함정 )