[스프링/Spring] Google OAuth2 로그인 구현하기(3) - OAuth2 핸들러와 서비스 구현

dongbrown·2024년 11월 24일

Spring

목록 보기
16/23

1. OAuth2 인증 핸들러 구현

1.1 Success Handler

@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                      HttpServletResponse response,
                                      Authentication authentication) throws IOException {
        OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
        
        try {
            // OAuth2User에서 정보 추출
            String email = oauth2User.getAttribute("email");
            String name = oauth2User.getAttribute("name");
            
            // 임시 토큰 생성 (GUEST 권한)
            String temporaryToken = jwtTokenProvider.createAccessToken(
                email,    // 이메일을 임시 ID로 사용
                null,     // memberNo는 추가 정보 입력 후 설정
                name,
                MemberRole.ROLE_GUEST
            );
            
            // 추가 정보 입력 페이지로 리다이렉트
            String targetUrl = UriComponentsBuilder.fromUriString("/auth/complete-profile")
                .queryParam("token", temporaryToken)
                .build().toUriString();
                
            log.debug("Redirecting to: {}", targetUrl);
            getRedirectStrategy().sendRedirect(request, response, targetUrl);
            
        } catch (Exception e) {
            log.error("OAuth2 인증 성공 처리 중 오류 발생", e);
            throw new OAuth2AuthenticationException("OAuth2 authentication failed");
        }
    }
}

1.2 Failure Handler

@Component
@Slf4j
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                      HttpServletResponse response,
                                      AuthenticationException exception) throws IOException {
        
        log.error("OAuth2 Authentication failure", exception);
        
        String errorMessage = URLEncoder.encode("소셜 로그인 실패: " + 
                                              exception.getLocalizedMessage(), 
                                              StandardCharsets.UTF_8);
        
        String targetUrl = UriComponentsBuilder.fromUriString("/auth/login")
                .queryParam("error", errorMessage)
                .build().toUriString();
                
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}

2. OAuth2 서비스 인터페이스 설계

public interface OAuth2Service {
    /**
     * OAuth2 사용자 정보 처리
     */
    LoginResponseDTO processOAuth2UserInfo(OAuth2User oauth2User);
    
    /**
     * OAuth2 사용자 정보 추출
     */
    OAuth2UserInfo extractOAuth2UserInfo(OAuth2User oauth2User);
    
    /**
     * 이메일로 신규 회원 여부 확인
     */
    boolean isNewUser(String email);
    
    /**
     * OAuth2 로그인 처리
     */
    LoginResponseDTO loginOAuth2User(OAuth2UserInfo userInfo);
    
    /**
     * OAuth2 추가 정보 입력 후 회원가입 완료
     */
    LoginResponseDTO completeRegistration(AdditionalUserInfoDTO additionalInfo);
}

3. OAuth2 서비스 구현

@Slf4j
@Service
@RequiredArgsConstructor
public class OAuth2ServiceImpl implements OAuth2Service {

    private final MemberMapper memberMapper;
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    @Transactional
    public LoginResponseDTO processOAuth2UserInfo(OAuth2User oauth2User) {
        OAuth2UserInfo userInfo = extractOAuth2UserInfo(oauth2User);
        
        try {
            // 기존 회원인지 확인
            MemberDTO existingMember = memberMapper.findByEmail(userInfo.getEmail());
            
            if (existingMember != null) {
                // 기존 회원이면 로그인 처리
                return loginOAuth2User(userInfo);
            } else {
                // 신규 회원이면 임시 회원 정보 생성
                MemberDTO temporaryMember = createTemporaryMember(userInfo);
                memberMapper.insertMember(temporaryMember);

                return LoginResponseDTO.builder()
                        .memberNo(temporaryMember.getMemberNo())
                        .id(temporaryMember.getEmail())
                        .name(temporaryMember.getName())
                        .role(temporaryMember.getRole())
                        .isNewUser(true)
                        .build();
            }
        } catch (Exception e) {
            log.error("OAuth2 사용자 처리 중 오류 발생", e);
            throw new OAuth2AuthenticationException("Failed to process OAuth2 user");
        }
    }

    @Override
    @Transactional
    public LoginResponseDTO completeRegistration(AdditionalUserInfoDTO additionalInfo) {
        MemberDTO member = memberMapper.findByEmail(additionalInfo.getEmail());
        if (member == null) {
            throw new RuntimeException("회원 정보를 찾을 수 없습니다.");
        }

        try {
            // 1. 기본 회원 정보 업데이트
            MemberDTO updateDto = MemberDTO.builder()
                    .memberNo(member.getMemberNo())
                    .id(additionalInfo.getId())
                    .name(member.getName())
                    .email(member.getEmail())
                    .phone(additionalInfo.getPhone())
                    .address(additionalInfo.getAddress())
                    .birthday(additionalInfo.getBirthday())
                    .gender(additionalInfo.getGender())
                    .photoUrl(additionalInfo.getPhotoUrl())
                    .role(MemberRole.ROLE_USER)
                    .build();
            
            memberMapper.updateMemberProfile(updateDto);

            // 2. 약관 동의 정보 저장
            memberMapper.insertAgreements(
                member.getMemberNo(),
                additionalInfo.isTermsAgreed(),
                additionalInfo.isPrivacyAgreed(),
                additionalInfo.isMarketingAgreed()
            );

            // 3. 개발자 정보 저장
            if (additionalInfo.getDevFields() != null && !additionalInfo.getDevFields().isEmpty()) {
                memberMapper.insertMemberDevFields(member.getMemberNo(), additionalInfo.getDevFields());
            }
            if (additionalInfo.getLanguages() != null && !additionalInfo.getLanguages().isEmpty()) {
                memberMapper.insertMemberLanguages(member.getMemberNo(), additionalInfo.getLanguages());
            }

            // 4. 최종 회원 정보 조회
            MemberDTO finalMember = memberMapper.findByMemberNo(member.getMemberNo());

            // 5. 최종 토큰 생성
            String accessToken = jwtTokenProvider.createAccessToken(
                finalMember.getId(),
                finalMember.getMemberNo(),
                finalMember.getName(),
                finalMember.getRole()
            );

            return LoginResponseDTO.builder()
                    .memberNo(finalMember.getMemberNo())
                    .id(finalMember.getId())
                    .name(finalMember.getName())
                    .role(finalMember.getRole())
                    .accessToken(accessToken)
                    .isNewUser(false)
                    .build();

        } catch (Exception e) {
            log.error("회원가입 완료 처리 중 오류 발생", e);
            throw new RuntimeException("회원가입 완료 처리 중 오류가 발생했습니다.");
        }
    }
}

4. 주요 로직 설명

4.1 OAuth2 로그인 처리 흐름

  1. Google 로그인 시도
  2. OAuth2 인증 성공
  3. Success Handler에서 임시 토큰 생성
  4. 사용자 존재 여부 확인
    • 기존 회원: 로그인 처리
    • 신규 회원: 추가 정보 입력 페이지로 이동

4.2 추가 정보 입력 처리

  1. 임시 토큰으로 사용자 식별
  2. 기본 정보 업데이트
  3. 약관 동의 정보 저장
  4. 개발자 정보 저장
  5. 최종 토큰 발급

4.3 보안 고려사항

  • 토큰의 권한 구분 (GUEST/USER)
  • 트랜잭션 관리
  • 예외 처리와 로깅

0개의 댓글