DevBoi

[Flutter] IOS 애플 로그인, 회원탈퇴 본문

[Mobile]/[Flutter]

[Flutter] IOS 애플 로그인, 회원탈퇴

HiSmith 2023. 12. 16. 11:01
반응형

IOS 의 심사 과정은 굉~장히 빡세다.

특히 애플은 소셜 로그인이 달려있으면, 애플 로그인이 무조건 달려있어야 하고 로그아웃시 

애플 과 연동을 끊었는지를 무조건 본다.

 

이에 애플 로그인 로그아웃을 정리한다.

애플 디벨로퍼 내용은 조금 어려울 수도있고, 복잡해 보이는데 

크게는 그냥 3가지이다. Certificates,Identifiers,Key 이렇게 추가를 해주고 서로 매핑 시켜준다고 보면된다.

 

 

[애플 로그인 하기]

 

애플 로그인

애플로그인은 뭐 기타 소셜 로그인과 같이 굉장히 쉽다. 

sign_in_with_apple: ^5.0.0

 

 

AppleDeveloper 설정 (Identifiers, key 생성하기)

 

이렇게 까지만 하면 일단, 애플 로그인을 위한 설정이 끝난다.

 

플러터 로그인 소스

나는 apple 관련 로그인 함수를 안드로이드 기기 예외 처리를했기때문에

애플 로그인 관련 함수만 처리되면 된다.

Future<String?> signInWithApple() async {
    try {
      final AuthorizationCredentialAppleID credential =
      await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
        webAuthenticationOptions: WebAuthenticationOptions(
          clientId: "bundle Id",
          redirectUri: Uri.parse(
            "firebase Auth에서 설정한 Id( 없어도됨)",
          ),
        ),
      );
      List<String> jwt = credential.identityToken?.split('.') ?? [];
      String payload = jwt[1];
      payload = base64.normalize(payload);
      final List<int> jsonData = base64.decode(payload);
      final userInfo = jsonDecode(utf8.decode(jsonData));
      String? email = userInfo['email'];
      String? fullName = userInfo['fullName'];

      if(email != null){
        userProvider.userInfo.email = email;
      }
      if(credential.givenName != null){
        userProvider.userInfo.name = credential.givenName;
      }
      if(credential.familyName != null){
        userProvider.userInfo.name = credential.familyName;
      }
      // if(userProvider.userInfo.name == '' || userProvider.userInfo.name == null){
      //   showToast(context, '애플 로그인 정보 불러오기 오류, 마이 밸류 앱의 애플로그인 정보를 삭제 해주세요');
      //   return '';
      // }
      return email ?? credential.userIdentifier;
    } catch (error) {
      print('error = $error');
    }
    return '';
  }

 

크게, jwt 토큰으로 분해해서 정보들을 세팅하는 것이다.

처음 로그인 시에는 정보가 존재하나, 이후에는 로그인해도 정보를 안주기때문에 초기에 회원 정보 세팅에 유의해야한다.

파이어 베이스 부분은 빼도되고 넣어도된다. 자유이다.

 

플러터 회원 탈퇴 소스

회원 탈퇴를 위해서는 아래와 같이진행했다.

1. flutter 에서 애플로그인 후 authorizeCode획득

2. 백엔드에서 받아서, 생성한 클라이언트 서비스와 함께 토큰 만료 revoke api 통신

3. 해당 통신이정상적으로 됬다면, 플러터 단에서 이후 탈퇴 처리

 

 

1. auth 코드 획득

Future<String?> getAuthCode() async {
    try {
      final AuthorizationCredentialAppleID credential =
      await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
        webAuthenticationOptions: WebAuthenticationOptions(
          clientId: "bundle id",
          redirectUri: Uri.parse(
            "파이어베이스 auth 설정 redirect url",
          ),
        ),
      );

      return credential.authorizationCode;
    } catch (error) {
      print('error = $error');
    }
    return '';
  }

 

2. 백엔드 호출

Future<String> appleQuitMember(String authCode) async{
  String result ='';
  var url = Uri.http(backendHost, '/app/apple/quit',{"authCode":authCode});
  await http.get(
    url,
  ).then((value) =>
  {
    result = value.body,
  });
  return result;
}

 

3. 탈퇴 전체 로직

  Future applequitMember() async{
    showToast(context,"애플로그인의 경우 한번 더 인증이 필요합니다.");
    getAuthCode().then((value) => {
      appleQuitMember(value!).then((result) => {
        if(result != '' && result != null){
          quitMember(userProvider.userInfo.id, quitCode, quitDesc).then((value) => {
            if(value){
              //userProvider 초기화
              userProvider.reset(),
              //로컬 디비 삭제
              localDbInterface.init().then((value) => {
                localDbInterface.deleteAll(),
              }),
              //페이지 스택 전부 날리고 초기화면으로 이동
              Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (BuildContext context) =>
                  SplashGuest()), (route) => false)
            }})
        }
        else{
          showToast(context,"애플 로그인 정보해제에 실패했습니다. 관리자에게 문의하거나, 설정에서 Myvalue 연동을 해제해주세요.")
        }
      })
    });
  }

 

 

4. 탈퇴 처리 백엔드 로직

package com.boiler.core.backend.app.mypage;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.json.JSONObject;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


import java.io.*;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

@RestController
@Slf4j
@RequestMapping("/app")
public class AppleLoginController {

  @GetMapping("/apple/quit")
  public boolean appleGetScret(@RequestParam("authCode") String authCode) throws IOException {
    String clientSecret =  this.createClientSecret();
    String refreshToken = this.getRefreshToken(clientSecret,authCode);
    this.withDrawApple(clientSecret,refreshToken);
    return true;
  }

  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", "ZXD6WGBEDL"); // kid
    jwtHeader.put("alg", "ES256"); // alg
    return Jwts.builder()
      .setHeaderParams(jwtHeader)
      .setIssuer("???????") // iss
      .setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간
      .setExpiration(expirationDate) // 만료 시간
      .setAudience("https://appleid.apple.com") // aud
      .setSubject("com.myvalue.myvalueapp") // sub
      .signWith(SignatureAlgorithm.ES256, getPrivateKey())
      .compact();
  }

  public PrivateKey getPrivateKey() throws IOException {
    //ClassPathResource resource = new ClassPathResource("static/AuthKey_YSK6W5BQDT.p8"); // .p8 key파일 위치
    //String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));
    String privateKey = """
     """; //애플디벨로퍼에서 받은 P8파일 전체 복사 붙여넣기


    Reader pemReader = new StringReader(privateKey);
    PEMParser pemParser = new PEMParser(pemReader);
    JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
    PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
    return converter.getPrivateKey(object);
  }
  public String getRefreshToken(String clientSecret, String authCode){

    String refreshToken = "";

    String uriStr = "https://appleid.apple.com/auth/token";

    Map<String, String> params = new HashMap<>();
    params.put("client_secret", clientSecret); // 생성한 clientSecret
    params.put("code", authCode); // 애플 로그인 시, 응답값으로 받은 authrizationCode
    params.put("grant_type", "authorization_code");
    params.put("client_id", "com.ddfasd.example"); // app bundle id

    try {
      HttpRequest getRequest = HttpRequest.newBuilder()
        .uri(new URI(uriStr))
        .POST(getParamsUrlEncoded(params))
        .headers("Content-Type", "application/x-www-form-urlencoded")
        .build();

      HttpClient httpClient = HttpClient.newHttpClient();
      HttpResponse<String> getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString());

      JSONObject parseData = new JSONObject(getResponse.body());
      refreshToken = parseData.get("refresh_token").toString();

    } catch (Exception e) {
      e.printStackTrace();
    }

    return refreshToken; // 생성된 refreshToken
  }

  private HttpRequest.BodyPublisher getParamsUrlEncoded(Map<String, String> parameters) {
    String urlEncoded = parameters.entrySet()
      .stream()
      .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
      .collect(Collectors.joining("&"));
    return HttpRequest.BodyPublishers.ofString(urlEncoded);
  }

  public void withDrawApple(String clientSecret, String refreshToken){

    String uriStr = "https://appleid.apple.com/auth/revoke";

    Map<String, String> params = new HashMap<>();
    params.put("client_secret", clientSecret); // 생성한 client_secret
    params.put("token", refreshToken); // 생성한 refresh_token
    params.put("client_id", "com.example.example"); // app bundle id

    try {
      HttpRequest getRequest = HttpRequest.newBuilder()
        .uri(new URI(uriStr))
        .POST(getParamsUrlEncoded(params))
        .headers("Content-Type", "application/x-www-form-urlencoded")
        .build();

      HttpClient httpClient = HttpClient.newHttpClient();
      HttpResponse response = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString());
      log.info(response.toString());
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

}

 

 

위 변수 설명

kid
https://developer.apple.com/account/resources/authkeys/list
위에서 봤던 것처럼 생성했던 키파일에 들어가서
VIEW KEY DETAILS 의 KEY ID 를 입력해주면 된다.(A 10-character key identifier)

 

alg
토큰을 만들때 쓰는 알고리즘이다. 애플에서 ES256를 고정으로 사용하라고 한다.

 

iss
https://developer.apple.com/account/#!/membership
MEMBERSHIP 안의 Team ID를 입력해주면 된다.(10-character Team ID)


iat
client_secret 생성시간 - 현재시간

 

exp
client_secret 만료시간 - 현재시간으로 부터 15777000 초(6개월) 보다는 작은 시간이 설정되어야 한다.
(소스상에선 30일로 설정되었다.)

 

sub
App의 Bundle ID 값. (ex: com.test.kedric) client_id와 동일하다

 

 

백엔드 소스는 정리 및 리팩토링이 필요하나, 우선 빠른 출시를 위해서 구현을 해둔 부분이다.

이렇게 애플 회원탈퇴 토큰 만료 및 로그인에 대한 구현을 마쳤다.

반응형

'[Mobile] > [Flutter]' 카테고리의 다른 글

[flutter] 앱에 광고 달기  (1) 2023.12.30
[Flutter] Icon launcher 사용하기  (0) 2023.12.19
[Flutter] Rendering Object 오류  (0) 2023.11.27
[Flutter] imagePicker 관련 설정  (0) 2023.11.22
[Flutter] Naver 로그인  (0) 2023.11.21