iOS APP 출시를 위해선, 기획과 개발뿐 아니라 진행중에
정책이라는 큰 장벽에 부딪히기도 합니다.
이번에는, 정책 중 2022년 6월 30일부터 시작된 가이드라인에 대해 알아보도록 하겠습니다.
2022년 6월 30일부터 APP에 회원탈퇴에 대한 기능이 필수로 요구됨을 가이드라인에서
참조 : https://developer.apple.com/news/?id=12m75xbj
안내해주고 있습니다. 해당 정책을 준수하기 위한 절차를 밟아보도록 합니다.
현재, APP 단은 flutter 로 작성되었으며, https://pub.dev/packages/sign_in_with_apple 패키지를 사용중입니다.
패키지를 사용하기 위해 해당 패키지홈페이지의 가이드라인에 따라 설정을 먼저 해줍니다.
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
버튼 클릭시, 해당 패키지의 메서드를 이용하여 ID의 Credential 정보를 받아오고 있습니다.
아직 이 과정은, Apple의 정책에 따르면 Back-End에 거치는 작업은 현재 수중에 드러나지 않은 상태입니다.
우선, Apple 에서 권장하는 프로세스는 아래 이미지와 같습니다.
flutter 패키지를 이용하여 구현시, glitch 서버 설정과 함께 우선적으로 App에 요청합니다.
그렇게 우리는 UserCredential 을 상단의 코드로 받아볼 수 있습니다. 이후, 해당 Credential 을 가지고
Back-End에서의 작업을 살펴보도록 하겠습니다.
우선적으로, 아래 코드는 SpringBoot를 기준으로 작성되었음을 알려드립니다.
문서를 확인해보시면 client_secret은 다음과 같은 내용을 포함하여 JWT형식의 토큰을 생성해야 합니다.
{
"alg": "ES256",
"kid": "XYZ123ABCD"
}
{
"iss": "ZYX987FDEG",
"iat": 1467179036,
"exp": 1493908100,
"aud": "https://appleid.apple.com",
"sub": "com.example.apps"
}
alg : ES256을 사용하도록 하겠습니다.
kid : Apple Developer 페이지에 명시되어있는 Key ID
→ https://developer.apple.com/account/resources/authkeys/list
iss : Developer 페이지에 명시되어있는 Team ID
→ https://developer.apple.com/account/#!/membership
iat : client secret이 생성된 일시를 입력. (현재시간)
exp : client secret이 만료될 일시를 입력. (현재시간으로 부터 6개월을 초과하면 안된다.)
aud : "https://appleid.apple.com".
sub : App의 Bundle ID 값을 입력.
저는 해당 값을 yml 로 미리 빼두었습니다.
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
URL
POST https://appleid.apple.com/auth/token
HTTP Body
The list of input parameters required for the server to validate the authorization code or refresh token.
Parts
key | type | required |
client_id | string | Required |
client_secret | string | Required |
code | string | |
grant_type | string | Required |
refresh_token | string | |
redirect_uri | string |
위의 URL을 확인해보시면, Apple에서의 Token 생성을 위한 API 정보가 명시되어 있습니다.
grant_type 을 명시해주어야 하는데,
authorization_code 를 사용시 code 파라미터를 사용해주시면 되고,
refresh_token 을 사용시 refresh_token 파라미터를 사용해주시면 됩니다.
https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens
POST https://appleid.apple.com/auth/revoke
HTTP Body
The list of input parameters required for the server to invalidate the token.
Parts
key | type | required |
client_id | string | Required |
client_secret | string | Required |
token | string | Required |
token_type_hint |
애플 회원 탈퇴시 토큰 해지처리를 위한 정보입니다.
해당정보들을 토대로 아래에서, 코드를 작성해주도록 하겠습니다.
[AppleUtil.java]
package com.example.appleloginspringboot.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class AppleUtil {
@Value("${apple.signin.bundle-id}")
private String appleAppBundleId;
@Value("${apple.signin.key-id}")
private String appleSignKeyId;
@Value("${apple.signin.team-id}")
private String appleTeamId;
@Value("${apple.signin.key-path}")
private String appleSignKeyFilePath;
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", appleSignKeyId);
jwtHeader.put("alg", "ES256");
return Jwts.builder()
.setHeaderParams(jwtHeader)
.setIssuer(appleTeamId)
.setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간 - UNIX 시간
.setExpiration(expirationDate) // 만료 시간
.setAudience("https://appleid.apple.com")
.setSubject(appleAppBundleId)
.signWith(getPrivateKey())
.compact();
}
public PrivateKey getPrivateKey() throws IOException {
ClassPathResource resource = new ClassPathResource(appleSignKeyFilePath);
String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));
Reader pemReader = new StringReader(privateKey);
PEMParser pemParser = new PEMParser(pemReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
return converter.getPrivateKey(object);
}
public HashMap<String, Object> generateAuthToken(String authorizationCode) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
RestTemplate restTemplate = new RestTemplateBuilder().build();
HashMap<String, Object> rtnMap = new HashMap<String, Object>();
String authUrl = "https://appleid.apple.com/auth/token";
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", authorizationCode);
params.add("client_id", appleAppBundleId);
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<String> response = restTemplate.postForEntity(authUrl, httpEntity, String.class);
HashMap respMap = objectMapper.readValue(response.getBody(), HashMap.class);
rtnMap.put("statusCode", response.getStatusCodeValue());
rtnMap.put("accessToken", respMap.get("access_token"));
rtnMap.put("refreshToken", respMap.get("refresh_token"));
rtnMap.put("idToken", respMap.get("id_token"));
rtnMap.put("expiresIn", respMap.get("expires_in"));
return rtnMap;
} catch (HttpClientErrorException e) {
log.error("Apple Auth Token Error");
HashMap respMap = objectMapper.readValue(e.getResponseBodyAsString(), HashMap.class);
rtnMap.put("statusCode", e.getRawStatusCode());
rtnMap.put("errorDescription", respMap.get("error_description"));
rtnMap.put("error", respMap.get("error"));
return rtnMap;
}
}
}
저의 경우, 위와같이 코드를 작성하여 컨트롤러로 호출하여 테스트를 해보았는데, 문제없이 진행됩니다.
아래는, 해당 코드를 작성하기 위해 추가사용된 패키지를 Gradle 기준으로 적어두었습니다.
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.bouncycastle:bcprov-jdk18on:1.72'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.72'
[Flutter] SVG 이미지 사용하기 (0) | 2023.01.31 |
---|---|
[Flutter] FCM - Push Notification (0) | 2022.12.20 |
[Flutter]Network 에 있는 Image 호출시 이미지 cache 문제해결 (0) | 2022.12.05 |
[Flutter] webview_flutter 사용중 iOS alert, confirm 띄우기 (0) | 2022.12.05 |
[Flutter] Sizer 사용후기 (0) | 2022.12.05 |