Apple OAuth 콜백 오류와 세션 유실 문제 해결
목차
Google이나 Kakao OAuth를 먼저 붙여본 경험이 있으면 Apple이 얼마나 다른지 첫날부터 느낀다. 나머지 provider들은 인가 코드를 GET 파라미터로 리다이렉트해주는데, Apple은 response_mode=form_post를 강제해서 콜백이 POST 바디로 들어온다. 이 차이 하나가 생각보다 여러 곳을 동시에 터뜨린다.
form_post를 강제하는 데는 Apple 나름의 이유가 있다. GET 리다이렉트 방식은 URL에 인가 코드가 노출되고 브라우저 히스토리나 서버 로그에 남을 가능성이 있다. POST 바디로 넘기면 그 위험이 줄어든다. 보안 측면에서는 더 낫지만, 서버 설정 면에서는 손댈 곳이 훨씬 많다.
form_post 하나가 터뜨린 세 가지
CORS. Apple 인증 흐름에서 POST를 실제로 날리는 건 사용자 브라우저지만, 서버 응답에 Apple 콜백 URL에 대한 허용 origin이 없으면 브라우저가 400으로 막는다. WebMvcConfig의 CORS 허용 URL 목록에 Apple 콜백 URL을 명시적으로 추가해야 통과된다. 이게 첫 번째 벽.
봇 차단 필터. 프로젝트에 BotBlockFilter가 붙어 있었는데, Apple OAuth 콜백 요청이 필터에 걸려 403이 됐다. Apple 서버 IP 대역이나 User-Agent를 수상한 요청으로 분류한 것. 콜백 경로를 필터 예외 목록에 추가하는 걸로 해결했다. 봇 차단 필터가 있는 서비스라면 Apple 연동 시 반드시 확인해야 할 포인트다.
세션 유실. form_post 방식에서는 리다이렉트 흐름이 GET과 달라 세션 쿠키 유지가 불안정할 수 있다. 콜백 처리 후 세션에 사용자 정보가 없는 상태가 됐고, OAuthController에서 처리 직후 세션을 명시적으로 재설정했다.
// OAuthController - Apple 콜백 후 세션 강제 재설정
session.setAttribute("loginUser", member);
session.setAttribute("sysId", member.getSysId());
수정 순서가 중요했다. 필터부터 뚫지 않으면 콜백 자체가 안 들어오고, CORS가 열려 있지 않으면 400에서 막힌다. 세션 재설정은 그다음 문제다. 동시에 여러 증상이 나와서 원인 파악이 늦어질 뻔했는데, 필터 - CORS - 세션 순서로 하나씩 로그 보면서 풀었다.
BotBlockFilter → Apple 콜백 URL 예외 추가 (403 해결)
WebMvcConfig → CORS 허용 URL 추가 (400 해결)
OAuthController → 세션 재설정 로직 추가 (로그인 유실 해결)
| 버그 | 증상 | 해결책 |
|---|---|---|
| 봇 필터 차단 | 콜백 403 에러 | BotBlockFilter 예외 처리 |
| CORS 차단 | 콜백 400 에러 | WebMvcConfig에 Apple URL 허용 |
| 세션 유실 | 로그인 실패 | 콜백 후 세션 강제 재설정 |
Apple만의 함정 - 이름과 이메일은 첫 번째 콜백이 마지막
OAuth provider를 여럿 붙이다 보면 사용자 이름이나 이메일을 가져오는 건 당연한 흐름이라고 생각하게 된다. Google이나 Kakao는 매 로그인마다 profile API를 호출해서 최신 정보를 가져올 수 있다. Apple은 완전히 다르다.
Apple은 최초 인증 시에만 이름과 이메일을 콜백 바디에 담아준다. 동일 Apple ID로 두 번째 로그인부터는 아무 정보도 오지 않는다. 사용자가 앱 연결을 해제했다가 다시 연결해도 마찬가지인데, 이미 한 번 인증한 계정이면 안 준다.
이걸 모르고 Google/Kakao 방식으로 "로그인마다 profile 갱신" 로직을 그대로 쓰면, 첫 로그인 이후 사용자 이름이 전부 null이 된다. 최초 콜백에서 반드시 저장해두고, 이후 콜백에서는 기존 DB 값을 그대로 써야 한다.
최초 여부 판단은 Apple이 직접 알려주는 게 아니라, DB에 해당 Apple sub 값으로 저장된 레코드가 있는지로 직접 판단해야 한다.
// Apple sub 기준으로 신규/기존 회원 분기
Optional<Member> existing = memberRepository.findByAppleSub(appleSub);
if (existing.isEmpty()) {
// 최초 인증 - name/email이 콜백 바디에 있음
Member newMember = new Member();
newMember.setAppleSub(appleSub);
newMember.setName(appleUser.getName()); // 이후 콜백엔 null
newMember.setEmail(appleUser.getEmail()); // 이후 콜백엔 null
memberRepository.save(newMember);
member = newMember;
} else {
// 재인증 - name/email은 null, 기존 레코드 그대로 사용
member = existing.get();
}
테스트할 때도 주의할 게 있다. Apple 테스트 계정으로 처음 로그인해서 이름이 잘 들어오는 걸 확인하고 넘어갔다가, 같은 계정으로 다시 로그인하면 null이 되는 걸 뒤늦게 발견하는 패턴이 있다. 개발 중에 반드시 두 번째 로그인 시나리오를 확인해야 한다. Apple 개발자 콘솔에서 앱 연결 해제 후 재연결하면 최초 인증을 다시 시뮬레이션할 수 있으니 이걸 활용하면 된다.
provider별 분기는 피할 수 없다
결국 Google, Kakao, Apple 세 provider를 한 OAuthController에서 처리하는 구조는 분기 없이는 못 간다. response_mode, 이름/이메일 수신 방식, 세션 처리까지 전부 다르다. 전략 패턴으로 provider별 구현을 숨기면 장기적으로는 깔끔하다.
// 추상화한다면 이런 방향
public interface OAuthHandler {
Member processCallback(HttpServletRequest request, HttpSession session);
String getProvider();
}
public class AppleOAuthHandler implements OAuthHandler {
@Override
public Member processCallback(HttpServletRequest request, HttpSession session) {
// form_post 처리, 최초 여부 판단, 세션 재설정
}
}
provider가 세 개밖에 없는 지금은 이렇게까지 하면 오버엔지니어링이다. 지금 단계에서 인터페이스 설계에 시간 쓰는 것보다 콜백 처리 메서드 내부에서 provider 타입을 확인하는 분기로 일단 처리했다. provider가 더 늘어나면 그때 정리할 영역.
이번 작업의 교훈은 단순하다. SNS 로그인은 provider 공식 문서를 먼저 읽어야 한다. Google OAuth 경험만 가지고 Apple을 시작하면 CORS, 봇 필터, 세션, 이름 수신 방식 이 네 개를 전부 뒤늦게 발견한다. Apple Sign In 공식 문서에 form_post 강제 사항과 이름/이메일 단회 전달 정책이 명시돼 있다. 시작 전에 읽었으면 삽질이 절반으로 줄었을 것이다.
댓글 0
첫 댓글 달아줘.