왜인지는 모르겠으나 이번 애플 소셜로그인 탈퇴 기능을 구현하면서 생각보다 레퍼런스가 적음을 알았고 저도 구현했던 개념을 공부하고 정리할 겸 블로그에 작성하게 되었습니다. 혹시나 궁금하신 점이나 틀린 점이 있으면 댓글로 남겨주시면 바로 반영하겠습니다
🤔 애플 소셜로그인 탈퇴 도입 과정
기존에 회원탈퇴 기능이 없었던 것은 아니었습니다. 기존의 회원탈퇴 로직은
1. 클라이언트 측에서 회원 탈퇴 요청 (REST API)
2. 서버 측에서 유저 정보 확인
3. 서버 측에서 해당 유저가 작성한 모든 답변 삭제 처리
4. 서버 측에서 해당 유저의 정보를 지우는 것이 아닌 STATUS 컬럼 UPDATE 처리
위 로직을 따르고 있었습니다.
하지만 애플 측에서 하는 말을 보면 애플 소셜로그인 과정에서 사용자 토큰을 넘겨주는데 이 토큰을 해지시켜야 한다는 것입니다. 만약 이런 과정이 없으면 앱스토어 심사에서 리젝을 당한다고 합니다.
https://developer.apple.com/kr/news/?id=12m75xbj
혹여나 프로젝트에 애플 소셜로그인을 넣고 앱 출시까지 가지고 있으신 분들은 꼭 위의 링크에 들어가셔서 확인해주세요!
💻 애플 소셜로그인 탈퇴 로직
본격적으로 탈퇴 구현기를 시작하기 전에, 전체적인 로직을 보여드리겠습니다. 저는 개발할 때 큰 로직부터 보고 가는 편인데, 이러면 지금 내가 짜고있는 부분이 어떤 과정인지 이해가 훨씬 더 잘되서 좋더라고요! 다만 애플 소셜로그인은 각 서비스마다 로직이 조금 다를 수 있는데(어떤 정보를 쓰고 안쓰고에 따라..) 각자 서비스에 맞게 봐주시면 감사하겠습니다!
1. 클라이언트는 탈퇴를 진행하기 위해 애플 소셜로그인 진행
2. 클라이언트는 서버에게 authorizationCode를 넘김
3. 서버는 애플 측에게 accessToken을 받아오는 REST API를 요청함
4. 서버는 애플 측에게 accessToken을 받아서 또 애플 측에게 탈퇴 REST API를 요청함
5. 애플 REST API에서 200이 내려오면 서버는 자체 회원 탈퇴(답변 삭제 및 유저 컬럼 업데이트) 진행
크게 이런 과정으로 진행됩니다. 다른 과정은 다 그럴 듯한데 탈퇴를 진행하기 위해 애플 소셜로그인을 한번 더 진행하는 것은 비효율적으로 보일 수 있습니다. 하지만 꼭 해줘야하는 과정인게, 탈퇴 REST API를 요청하기 위해서는 애플 측에서 주는 authorizationCode가 필수로 필요한데 이는 유효시간이 5분밖에 안된다고 합니다. 그래서 처음 로그인할 때 받은 값을 탈퇴 때 쓰면 당연히 유효시간이 지난 값이기 때문에 탈퇴에 실패한다고 합니다.
🏃♂️ 애플 소셜로그인 탈퇴 구현
자! 이제 어느정도 전체적인 구조는 파악했으니 이제 구현기로 넘어가겠습니다. 저번 푸시알림과 마찬가지로 저는 서버측 구현만 다루도록 하겠습니다. 위 로직 순서를 보면 서버가 구현해야할 부분은 3,4,5번으로 클라이언트한테 정상적으로 authorizationCode를 받았다는 가정으로 진행하겠습니다.
이번 애플 소셜로그인 탈퇴를 구현하며 가장 힘들었던 점은 import 과정입니다.. 다들 이게 왜 힘드냐고 여쭤보실 수 있지만 대부분의 레퍼런스에서 build.gradle에 어떤 라이브러리를 넣어야하는지 안가르쳐줘서 한참을 해맸습니다.. 결국 해결을 못해 stackOverFlow에 인도인 형님들과 긴 대화를 나눈 경험도 있습니다..😢
쨌든, build.gradle부터 보여드리면
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
}
이 두 라이브러리를 받아서 진행했습니다.
그 다음 3번 과정을 살펴보면 "서버는 애플 측에게 accessToken을 받아오는 REST API를 요청함" 인데 이는 애플에 탈퇴 REST API를 보낼 때 보안 상의 이유로 accessToken을 같이 넘겨야하기 때문에 진행하는 과정입니다. 코드를 보여드리자면,
public AppleAuthTokenResponse GenerateAuthToken(String authorizationCode) throws IOException {
RestTemplate restTemplate = new RestTemplateBuilder().build();
String authUrl = "https://appleid.apple.com/auth/token";
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", authorizationCode);
params.add("client_id", BUNDLEID);
params.add("client_secret", createClientSecret());
params.add("grant_type", "authorization_code");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
try {
ResponseEntity<AppleAuthTokenResponse> response = restTemplate.postForEntity(authUrl, httpEntity, AppleAuthTokenResponse.class);
return response.getBody();
} catch (HttpClientErrorException e) {
log.error(String.valueOf(e));
throw new CustomException(ErrorCode.APPLE_WITHDRAW_FAILED);
}
}
public String createClientSecret() throws IOException {
Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant());
Map<String, Object> jwtHeader = new HashMap<>();
jwtHeader.put("kid", KID); // kid
jwtHeader.put("alg", "ES256"); // alg
return Jwts.builder()
.setHeaderParams(jwtHeader)
.setIssuer(ISS) // iss
.setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간
.setExpiration(expirationDate) // 만료 시간
.setAudience("https://appleid.apple.com") // aud
.setSubject(BUNDLEID) // sub
.signWith(SignatureAlgorithm.ES256, getPrivateKey())
.compact();
}
위 코드에서 BUNDLEID, ISS 같은 경우에는 애플 디벨로퍼에서 제공해주는 개인 정보이기 때문에 gitignore로 숨김처리 한뒤, @Value로 가져왔습니다!
위 코드를 요약해보자면, 클라이언트 한테 받은 authorizationCode와 애플디벨로퍼에서 제공해주는 개인 정보를 가지고 파라미터로 넣고
https://appleid.apple.com/auth/token
위 REST API URI로 요청을 보내는 것을 알 수 있습니다.
위의 API로 요청을 보냈을 때 응답값이 잘 내려오면 거기서 내려온 accessToken을 사용하고 아니면 저희 서버팀만의 customException을 던져줬습니다.
@Getter
public class AppleAuthTokenResponse {
String accessToken;
Integer expires_in;
String id_token;
String refresh_token;
String token_type;
}
위 AppleAuthTokenResponse로 내려오는데 이는 애플 공식문서에 잘나와있으니 참고해보시면 좋을 것 같습니다.!
그 다음 4번 과정으로 가보면
public void revoke(String authorizationCode) throws IOException {
AppleAuthTokenResponse appleAuthToken = GenerateAuthToken(authorizationCode);
if (appleAuthToken.getAccessToken() != null) {
RestTemplate restTemplate = new RestTemplateBuilder().build();
String revokeUrl = "https://appleid.apple.com/auth/revoke";
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", BUNDLEID);
params.add("client_secret", createClientSecret());
params.add("token", appleAuthToken.getAccessToken());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
restTemplate.postForEntity(revokeUrl, httpEntity, String.class);
}
}
위 코드입니다. 코드를 보시면 방금 accessToken을 요청하는 메소드 GenerateAuthToken을 보실 수 있습니다.
해당 메소드로부터 accessToken을 받고 "https://appleid.apple.com/auth/revoke" API로 탈퇴 요청을 보내는 것을 알 수 있습니다.
물론 이 과정에서도 파라미터로 BUNDLEID와 clientSecret을 집어넣는 것을 볼 수 있습니다. 이렇게 성공적으로 요청이 갔으면 애플 측에서 사용자 토큰을 해지 처리를 해줍니다. 여기까지만 해주면 앱스토어 리젝을 안당한다고 합니다! ㅎㅎ
5번 과정은 저희 서비스 만의 자체 약속이므로 생략하도록 하겠습니다!
👊🏻 애플 소셜로그인 탈퇴 후기
앱 스토어에 제출하고 승인받으려면 정말 많은 노력을 해야함을 요즘 깨닫고 있습니다. 하나부터 열까지 애플은 정말 깐깐하게 보는 느낌인데 저희 팀은 iOS 어플을 주력으로 타겟킹하고 있기 때문에 개발팀에서 조금 더 노력해야하는 것이 사실입니다 😭
오늘은 2주정도 전에 구현했던 애플 소셜로그인 탈퇴를 정리해봤습니다. 사실 벌써 가물가물해지기 시작해서 얼른 써버리자라고 생각하고 썼던 것인데 궁금하신 점이나 틀린 점은 바로바로 댓글 남겨주시면 수정처리하겠습니다! 감사합니다:)
'구현' 카테고리의 다른 글
[구현] 싱글톤(Singleton) 패턴 직접 적용해보기 (0) | 2023.12.06 |
---|---|
[구현] BaseTimeEntity로 불필요한 코드 줄이기 (JPA) (0) | 2023.11.08 |
[구현] 푸시알림(FCM) / SpringBoot (0) | 2023.06.17 |