본문 바로가기
Language/Spring

[SPRING] 애플 로그인 API 쉽게 구현 방법 및 예제 - OAuth 2.0, Javascript, Jsp

by tyrannojung 2021. 8. 20.
반응형

실제 구현 예

 

 IOS 앱 배포를 위해 본인의 어플을 애플에 심사함에 있어, 소셜 로그인 기능을 사용하지만 애플 로그인이 없으면 reject사유가 되므로 전혀 고려하지 않은 애플 로그인을 만들게 되었습니다.

 

여러 자료들을 종합하고 본인의 시행착오를 거쳐 최대한 정제해서 정리했습니다.

 

동작 순서

애플 로그인 호출 -> 애플로그인 -> 성공 -> 설정한 Redirect 호출(자바) -> 애플에서 정보 받고 파싱 후 처리

 

미리 준비할 것.

 

1. TEAM_ID

2. CLIENT_ID

3. REDIRECT_URL

4. KEY_ID

 

 

앞전에 설정한 애플 디벨로퍼에서 가져와야 될 자료들이 있습니다.

 

[초간단] 애플 로그인 API 연동 초기 설정 하기

 IOS 앱 배포를 위해 본인의 어플을 애플에 심사함에 있어, 소셜 로그인 기능을 사용하지만 애플 로그인이 없으면 reject사유가 되므로 전혀 고려하지 않은 애플 로그인을 만들게 되었습니다. 여

tyrannocoding.tistory.com

(해당 링크 참조)

 

 

 

TEAM_ID

 

 

본인의 Identifiers을 클릭하면 나옵니다.

 

 

CLIENT_ID & REDIRECT_URL

 

 

 

identifiers -> services ids 누릅니다.

 

CLIENT_ID는 위의 주황색 박스(services의 identifier)이며,
REDIRECT_URL은 해당 identifier선택 후, sign in with apple 옆 configure에서 확인 가능합니다.

 

KEY_ID

좌측 메뉴의 keys에서 키를 누르면 확인 가능합니다.

 

코드

 

pom.xml

 

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
</dependency>

<dependency>
	<groupId>org.json</groupId>
	<artifactId>json</artifactId>
	<version>20210307</version>
</dependency>

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

 

 

Java(Spring)

 

KEY생성시 다운받았던 AuthKey 경로

@Controller
public class AppleController {
	
	/**
	 *  추후 해당 내용을 properties에 옮기시는것을 추천드립니다.
	 */
	
	public static final String TEAM_ID = "9NS2G9S8SM";
	public static final String REDIRECT_URL = "https://wmall.nodehome.io/login/oauth_apple";
	public static final String CLIENT_ID = "io.nodehome.services";
	public static final String KEY_ID = "2RJ9H5FGB8";
	
    <!-- 고정 -->
	public static final String AUTH_URL = "https://appleid.apple.com";
    <!-- 앞전에 다운받은 AuthKey.p8 경로 -->
	public static final String KEY_PATH = "static/apple/AuthKey_2RJ9H5FGB8.p8";
	
	/**
	 * Controller
	 */

	@RequestMapping(value = "/appleRestLogin", method = RequestMethod.GET)
	public String kakaoRestLogin(
			Model model
			, Locale locale) {
		
		return "etc/social/apple/apple_login";
	}
	
	@RequestMapping(value = "/login/getAppleAuthUrl")
	public @ResponseBody String getAppleAuthUrl(
			HttpServletRequest request) throws Exception {
		
	    String reqUrl = 
	    		AUTH_URL 
	    		+ "/auth/authorize?client_id=" 
	    	    + CLIENT_ID
	    	    + "&redirect_uri=" 
	    	    + REDIRECT_URL
	            + "&response_type=code id_token&scope=name email&response_mode=form_post";

	    return reqUrl;
	}

	@RequestMapping(value = "/login/oauth_apple")
	public String oauth_apple(
			HttpServletRequest request
			, @RequestParam(value = "code", required= false) String code
			, HttpServletResponse response
			, Model model) throws Exception {
		
		// 애플로그인 취소시 로그인페이지로 이동
		if(code == null) {
			return "/test/test";
		}
		
	    String client_id = CLIENT_ID;
	    String client_secret = createClientSecret(TEAM_ID, CLIENT_ID, KEY_ID, KEY_PATH, AUTH_URL);
	    
	    // 토큰 검증 및 발급
	    String reqUrl = AUTH_URL + "/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 = doPost(reqUrl, tokenRequest);
	    
	    JSONObject tokenResponse = new JSONObject(apiResponse);
        JSONObject appleInfo = decodeFromIdToken(tokenResponse.getString("id_token"));
        
        /** 
        
        TO DO : 리턴받은 appleInfo로 
        		, 회원가입처리
        		, 로그인처리
        		, 처리 후 원하는 위치 이동
        */
	    
	    
	    // 추후 아래는 삭제
        model.addAttribute("appleInfo", appleInfo);
        return "/test/test";

	}
	////////////////////////////////////////
	
	
	/**
	 * Util
	 */
	
    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;
        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();
            HttpEntity entity = response.getEntity();
            result = EntityUtils.toString(entity, "UTF-8");

            if (statusCode != 200) {
                System.out.println("애러");
            }
            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();
    	    String appleInfo = getPayload.toJSONObject().toJSONString();
    	    JSONObject payload = new JSONObject(appleInfo);

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

        return null;
    }
}

 

Html & Javascript

 

<html>

<head>
	<title>Home</title>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
	<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
  <body>
  <ul>
      <li onclick="appleLogin(); return false;">
        <a href="javascript:void(0)">
            <span>Login with Apple</span>
        </a>
      </li>
  </ul>

<script>
 function appleLogin() {
    $.ajax({
        url: '/login/getAppleAuthUrl',
        type: 'get',
        async: false,
        dataType: 'text',
        success: function (res) {
            location.href = res;
        }
    });
}

  $(document).ready(function() {
      var appleInfo = '${appleInfo}';
	  if(appleInfo != ""){
		var data = JSON.parse(appleInfo);
		if(data['sub'] != null) {
			alert("애플로그인 성공 \n accessToken : " + data['sub']);
			alert(
			"email : "
			+ data['email']);
		}
  });  
</script>
  </body>
</html>

 

시행착오

 

 애플 로그인을 진행할때, 다른 로그인들과 다르게 굉장히 어렵고 자료가 많이 없어 힘들었습니다. 최대한 코드를 복사 붙여넣기만 해도 돌아갈 수 있도록 작성해보고자 노력했습니다. 기타 패키지 나누는 작업과, property설정만 하면 이정도 예제만 가지고도 훌륭한 애플로그인을 만들 수 있을 거라 확신합니다.

 

 진행 중 가장 애먹었던 부분은, sun.security.ec.ECPrivateKeyImpl; 을 임포트 하지 못해 ECPrivateKeyImpl를 가져오지 못한 부분이었습니다.

 

이건 간단하게 project - properties에 들어가 java build path에 있는 Libraries 탭에서 JRE System Libery를 remove 한 후, Add Library를 클릭하여 JRE System Library를 다시 추가만 해주면 정상적으로 가져올 수 있는 부분을 확인할 수 있습니다.

 

 

 

 여러 가지를 참고하여 만든 것으로 혹시라도 잘못됐거나 안 되는 점 있으면 댓글 남겨주시면 감사하겠습니다!

 

반응형

댓글