[OAuth2] Spring boot Apple Login

곽우현·2022년 1월 4일
4

개발일지

목록 보기
1/2
post-thumbnail

구글, 페이스북에 이어 애플로그인 연동한다.
구글 페이스북은 이 글 이후에 정리해서 올려야겠다.

일단 애플로그인은 구글, 페이스북보다 준비해야할 것들이 있다.
그 부분은 너무 잘 정리해주신 분이 계셔서 첨부한다.
[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정

설정을 하면서 조금 해맸던 부분이 있다.

그분의 이미지를 토대로 본다면 세번째 부분에 Return URLs 에 원하는 주소를 입력 후 ,(쉼표)를 추가해야 넘어가진다.

Return URLs에 https://{domain}/login/oauth2/apple 을 추가하였다.

@Slf4j
@Component
public class AppleUtil {
    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) {
                log.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;
    }
}

apple은 로그인을 하면 codeid_token을 주는대 이 코드?토큰으로 다시 애플에 보내 access-token을 가져오는거 같다.
그러나 나는 나만의 jwt token을 주므로 이 부분을 사용하지 않고 id_tokendecode하여 필요한 정보만 사용하는 것으로 바꾸었다.

@RequestMapping(value = "/oauth2/apple", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public void oauth2AppleLoginRedirect(String user, String code, String id_token,
                                         HttpServletResponse response) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        JSONObject data = appleUtil.decodeFromIdToken(id_token);
        if (user != null) {
            log.info("apple login first time : {}", user);
            // 처음으로 apple로그인을 하면 user 데이터가 있으니 디비에 저장
            AppleUserInfo appleUserInfo = objectMapper.readValue(user, AppleUserInfo.class);
            ...
            } else {
                log.info("already signup with web and already update oauth2 key");
	    // 처음이 아니므로 적절한 인증 후 넘긴다.
            ...
            }
        }
        String accessToken = jwtTokenProvider.createToken(userDetails.getUserId(), userDetails.getUserEmail(), userDetails.getUsername(), userDetails.getRole());
        String refreshToken = jwtTokenProvider.createRefreshToken(userDetails.getUserId(), userDetails.getUserEmail(), userDetails.getUsername(), userDetails.getRole());
        // header set
        ResponseCookie accessTokenCookie = ResponseCookie.from("access-token", accessToken)
                .path("/")
                .secure(true)
                .sameSite("None")
                .httpOnly(false)
                .domain("{domain}")
                .build();
        ResponseCookie refrshTokenCookie = ResponseCookie.from("refresh-token", refreshToken)
                .path("/")
                .secure(true)
                .sameSite("None")
                .httpOnly(false)
                .domain("{domain}")
                .build();
        try {
	// redis set
            redisTemplate.opsForValue().set(userDetails.getUserEmail(), refreshToken, jwtTokenProvider.refreshTokenValidTime, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.error("redis exception. please check redis server. : {},", user);
        }
        response.setHeader("Set-Cookie", accessTokenCookie.toString());
        response.addHeader("Set-Cookie", refrshTokenCookie.toString());
        response.sendRedirect(redirectUri);
    }

애플은 최초 처음 로그인할때만 유저정보를 준다. 유저 정보를 가지고 Database에 저장 후 토큰을 발급해주는 식으로 만들었다.
유저 정보는 www-form-urlencoded 방식으로 user에 담아준다.
다른 블로그를 참고하여 AppleUtil에서 많은 일을 하지만.. 나에게도 맞는 부분인지는 잘 몰라 이렇게 처리를 하였다..

profile
주니어 Java 개발자

2개의 댓글

comment-user-thumbnail
2022년 4월 15일

안녕하세요 좋은 글 잘 봤습니다! 혹시 ECPrivateKeyImpl 관련해서 오류는 없으셨나요?

1개의 답글