2 분 소요

Apple 로그인 구현하기

Apple Developer 설정

다른 소셜 로그인처럼 애플 로그인을 지원하려면 개발자 사이트에서 별도로 설정을 해줘야 한다!
하지만 설정을 해주려면 애플 개발자 걔종 등록을 해줘야 한다😭 1년에 129,000원이니 참고하자!
WHITEPAEK님의 글 1편에 자세히 설명돼 있으니 천천히 따라서 해보자.
참고로 Services 등록할 때 Return URLs에 들어갈 주소가 http를 사용중이거나, 도메인이 들어갈 위치에 IP가 오면 안된다.
그래서 http://localhost:8080https://{IP 주소}는 설정할 수 없다.

apple.html

애플 로그인 버튼을 표시할 테스트용 페이지를 resources/templates/apple.html로 등록해주자.
이후에 확인해보면 Sign in with Apple 버튼이 표시될 것이다!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Apple sign-in</title>
</head>
<body>
<div id="appleid-signin" data-color="black" data-border="true" data-type="sign in" style="height: 50px; width: 100%;"></div>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
<script type="text/javascript">
    AppleID.auth.init({
        clientId : '[[${client_id}]]',
        scope : 'name email',
        redirectURI : '[[${redirect_uri}]]',
        state : 'test',
        nonce : '[[${nonce}]]',
        usePopup : false
    });
</script>
</body>
</html>

이후 설정도 WHITEPAEK님의 글 2편를 참고해서 구현했는데,
우리 프로젝트에서는 JWT를 백엔드 서버에서 새로 발급해 활용할 계획이어서 조금 수정했다.

AppUtils

위에서 만든 apple 로그인 버튼에 필요한 정보를 넣어줄 getMetaInfo()
이후에 최종적으로 발급받을 JWT에서 email 정보를 추출하는 getEmailFromIdToken()를 정의해주자.
email을 반환해줄 필요는 없으나 진행중인 프로젝트에서 사용자 ID가 email이어서 그렇게 설정했다.

@Component
public class AppUtils {

    @Value("${APPLE.AUD}")
    private String AUD;
    @Value("${APPLE.WEBSITE_URL}")
    private String APPLE_WEBSITE_URL;

    public Map<String, String> getMetaInfo() {

        Map<String, String> metaInfo = new HashMap<>();

        metaInfo.put("CLIENT_ID", AUD);
        metaInfo.put("REDIRECT_URI", APPLE_WEBSITE_URL);
        metaInfo.put("NONCE", "20B20D-0S8-1K8"); // Test value

        return metaInfo;
    }

    public String getEmailFromIdToken(String id_token) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(id_token);
            JWTClaimsSet getPayload = signedJWT.getJWTClaimsSet();
            return getPayload.toJSONObject().get("email").toString();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}
  • AppUtils를 다른 곳에서 주입받아 활용할 수 있도록 @Component 설정
  • AUD, APPLE_WEBSITE_URL에 들어갈 값은 application.yml에서 가져오도록 @Value 설정
  • getEmailFromIdToken() : id_token이라는 JWT를 디코딩 후 email 정보만 반환

OAuthService

apple 로그인 관련 비즈니스 로직을 작성해주자.
모두 컨트롤러에서 활용할 함수들이며, AppUtils에서 작성했던 함수들을 활용하는걸 확인할 수 있다.

@Service
@RequiredArgsConstructor
public class OAuthService {

    private final AppUtils appUtils;
    private final UserRepository userRepository;
    private final MeasurementRepository measurementRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    public Map<String, String> getLoginMetaInfo() {
        return appUtils.getMetaInfo();
    }

    public User saveUser(String email, String provider) {
        Measurement measurement = measurementRepository.save(new Measurement());
        return userRepository.save(User.builder()
                        .password(passwordEncoder.encode(UUID.randomUUID().toString()))
                        .email(email)
                        .roles(new ArrayList<>(List.of("USER")))
                        .build());
    }

    public String getEmail(String id_token) {
        return appUtils.getEmailFromIdToken(id_token);
    }
}
  • UserRepository.save() : 스프링 데이터 JPA에서 기본으로 제공하는 저장 함수
  • Spring Security 사용중이므로 passwordBCryptPasswordEncoder로 인코딩한 값을 저장
  • 권한에 따라 접속할 수 있는 URI를 제한하고 있어서 roles를 별도로 설정했음 (선택)

OAuthController

apple 로그인 버튼을 apple.html에서 조회할 수 있도록 appleLoginPage()를 정의해주고,
사용자가 apple 로그인에 성공했을 때 redirect 될 URI에서 동작하는 함수 appleServiceRedirect()를 정의해주자.

로그인에 실패했다면 appleServiceResponsenull을 반환하고, 그렇지 않으면 id_token을 포함한 여러 값을 반환한다.
즉, apple 서버에서 JWT를 appleServiceResponse에 담아 보내주는데 인코딩된 값이므로 정의했던 getEmail()를 사용하자.

로그인한 정보와 일치하는 유저가 DB에 없다면 가입해서 로그인할 수 있도록 처리를 해줬다.
최종적으로 어디로 이동할지는 자유지만 우리 프로젝트에서는 /login에서 쿼리 스트링으로 넘어오는 token 값이 유효한 경우
메인 페이지로 redirect 되도록 설정했기 때문에 다음처럼 설정해줬다.

@Controller
@RequiredArgsConstructor
public class AppleController {

    private final OAuthService oAuthService;
    private final UserService userService;
    private final JwtTokenProvider jwtTokenProvider;
    private final String MAIN_LOGIN_URL = "https://example.com";

    @GetMapping("/apple")
    public String appleLoginPage(ModelMap model) {

        Map<String, String> metaInfo = oAuthService.getLoginMetaInfo();

        model.addAttribute("client_id", metaInfo.get("CLIENT_ID"));
        model.addAttribute("redirect_uri", metaInfo.get("REDIRECT_URI"));
        model.addAttribute("nonce", metaInfo.get("NONCE"));

        return "apple";
    }

    @PostMapping("/login/apple")
    public String appleServiceRedirect(AppleServiceResponse appleServiceResponse) {
        if (appleServiceResponse == null) {
            return "redirect:" + MAIN_LOGIN_URL;
        }

        String email = oAuthService.getEmail(appleServiceResponse.getId_token());
        User user = userService.findByEmail(email);

        if (user == null) {
            User newUser = oAuthService.saveUser(email, "apple");
            return "redirect:" + MAIN_LOGIN_URL
                    + "?token=" + jwtTokenProvider.createToken(newUser.getEmail(), newUser.getRoles());
        }
        return "redirect:" + MAIN_LOGIN_URL
                + "?token=" + jwtTokenProvider.createToken(user.getEmail(), user.getRoles());
    }
}

References

카테고리:

업데이트:

댓글남기기