Coding History/project

회원가입 로그인 로직 중, 중복 이메일 처리.

BlackBirdIT 2024. 9. 26. 01:50

지금 뭐 중복 이메일 처리 자체는 시큐리티 때문에 잘 되는데 로그인과 회원가입만 막는거지 알람 같은건 뜨지 않아서 유저가 영문도 모른채 다른 아이디를 사용해야된다는 불편함이 있다.

문제 발견 이유.

현재 내 스포티 파이 계정의 이메일과 세개의 구글 계정중 하나가 이메일이 같아서 알게 되었다.

그래서 기존 Ut 클래스를 사용해서 알람을 띄워보려고 했다.

        if (localUser.isPresent()) {
            return new CustomOAuth2User(localUser.get(), oAuth2User.getAttributes());
        } else {
            // 이메일 중복 확인은 새로운 회원을 등록할 때만 실행
            if (memberRepository.findByEmail(email).isPresent()) {
                throw new OAuth2AuthenticationException(Ut.jsHistoryBack("ERROR", "이미 사용 중인 이메일입니다."));
            }

회원 검증 단계에서 이런 로직을 추가해봤는데 알림이 뜨지 않았다.

그래서 알아보니까

OAuth2AuthenticationException은 주로 서버 측에서 발생하므로, 클라이언트로 직접 메시지를 전달하는 데 적합하지 않다고 한다.

아 간단할 줄 알았는데 이것도 시큐리티 단계에서 해결해야된다.

CustomOAuth2FailureHandler 클래스 선언.. 일이 커질 것 같은디.

오케이 일단 코드 다 짜서 실행을 해봤는데 여전히 안된다.

생각의 흐름

지금 내 상황 말해줄게 내 구글 아이디 il-------@gmail.com이 있고,

스포티 파이로 로그인할 시에도 il-----@gmail.com 이 이메일을 써,

그래서 db 초기화 이후에 둘 중 하나가 먼저 로그인을 시도하면 db에 회원으로 등록되어서 하나는 로그인이 실패가 자동으로 돼, 왜냐면 이메일이 중복이라서, 이게 시큐리티에서는 실패인 로직으로 보지 않는 것 같아. 로그인은 실질적으로 실패가 되어서 로그인 페이지로 리다이렉트 되지만 login으로 리다이렉트 되지 login?error로 리다이렉트 되지 않아서 이유를 정확히 유저가 알 수가 없어 이런 경우면 어떻게 해결해야되냐 가 생각의 흐름.

원인이 Google과 Spotify 둘 다 같은 이메일 주소로 로그인 시도할 경우, DB에서 해당 이메일로 이미 사용자가 등록되어 있을 때 발생하는 문제. 이메일 중복 때문에 로그인은 실패하지만, Spring Security에서는 OAuth 인증 자체가 성공적으로 처리되었기 때문에 이 상황을 인증 실패로 간주하지 않고 단순히 로그인 페이지로 리다이렉트만 시킨다.

그래서 이메일 중복을 OAuth 인증 실패로 처리해줘야 함.

우선 결과적으로 성공했다.

public class EmailAlreadyExistsException extends OAuth2AuthenticationException {
    public EmailAlreadyExistsException(String msg) {
        super(new OAuth2Error("email_exists", msg, null));
    }
}

해당 클래스로 이메일 중복 상황은 OAuth2Error라고 명시해주고,

CustomOAuth2UserService 클래스에서 기존 회원 가입 로직에 조건을 추가,

        if (memberRepository.findByEmail(email).isPresent()) {
            // 중복된 이메일이 존재할 경우 커스텀 예외를 발생시켜 Spring Security에서 실패로 처리하도록 함
            throw new EmailAlreadyExistsException("이미 사용 중인 이메일입니다.");
        }

이렇게, 이후에 CustomOAuth2FailureHandler를 선언해서 실패 핸들러를 커스텀으로 제작.

@Component
public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 예외 메시지를 추출
        String errorMessage;

        if (exception instanceof EmailAlreadyExistsException) {
            errorMessage = exception.getMessage();  // 이메일 중복 메시지 사용
        } else {
            errorMessage = "알 수 없는 이유로 로그인에 실패했습니다.";
        }

        // 오류 페이지로 리다이렉트 또는 알림
        response.sendRedirect("/usr/member/login?error=true&message=" + URLEncoder.encode(errorMessage, "UTF-8"));
    }
}

그리고 시큐리티 단계에서 OAuth2 인증 로직에 해당 핸들러를 추가해주고,

.oauth2Login(oauth2 -> oauth2
    .loginPage("/usr/member/login")
    .successHandler(oAuth2AuthenticationSuccessHandler)  // 로그인 성공 후 핸들러
    .defaultSuccessUrl("/usr/home/main", true)
    .failureHandler(customOAuth2FailureHandler)
    .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(customOAuth2UserService))
                )
    <!-- 오류 메시지 표시 -->
    <c:if test="${param.error eq 'true'}">
        <p style="color: red; font-weight: bold;">
            <c:choose>
                <c:when test="${not empty param.message}">
                    ${param.message}
                </c:when>
                <c:otherwise>
                    로그인에 실패했습니다.
                </c:otherwise>
            </c:choose>
        </p>
    </c:if>

login.jsp에서 제일 상단에 오류메세지 경우를 c:if로 만들어주면!

아까의 이미지와 같은 결과를 얻을 수 있다.

근데 다른 문제가 생김.

회원가입을 하면서 로그인은 되는데 회원 가입 이후에는 다 차단해버린다 ㅋㅋㅋㅋ

이건 조건문의 수정이 조금 필요해보인다.

        // 이미 기존 이메일로 로그인한 사용자가 있을 경우, 기존 사용자로 로그인 처리
        if (localUser.isPresent()) {
            return new CustomOAuth2User(localUser.get(), oAuth2User.getAttributes());
        }

조건문의 더 상위에 해당 로직을 추가해서 문제 없이 로그인 된다!!

    // Google 사용자 처리
    private OAuth2User processGoogleUser(OAuth2User oAuth2User, String email, String googleLoginId) {
        Optional<Member> localUser = memberRepository.findByGoogleLoginId(googleLoginId);

        // 이미 기존 이메일로 로그인한 사용자가 있을 경우, 기존 사용자로 로그인 처리
        if (localUser.isPresent()) {
            return new CustomOAuth2User(localUser.get(), oAuth2User.getAttributes());
        }

        if (memberRepository.findByEmail(email).isPresent()) {
            // 중복된 이메일이 존재할 경우 커스텀 예외를 발생시켜 Spring Security에서 실패로 처리하도록 함
            throw new EmailAlreadyExistsException("이미 사용 중인 이메일입니다.");
        }

        System.out.println("로컬 DB에 Google 사용자가 존재하지 않음, 새로운 사용자 등록 시작");

        String displayName = oAuth2User.getAttribute("name") != null ? oAuth2User.getAttribute("name") : "사용자";
        String nickname = email.split("@")[0];
        createGoogleUserInLocalDB(email, displayName, nickname, googleLoginId);

        Member newUser = memberRepository.findByGoogleLoginId(googleLoginId).orElseThrow();
        return new CustomOAuth2User(newUser, oAuth2User.getAttributes());
    }

    // Spotify 사용자 처리
    private OAuth2User processSpotifyUser(OAuth2User oAuth2User, String email, String spotifyLoginId) {
        Optional<Member> localUser = memberRepository.findBySpotifyLoginId(spotifyLoginId);

        // 이미 기존 이메일로 로그인한 사용자가 있을 경우, 기존 사용자로 로그인 처리
        if (localUser.isPresent()) {
            return new CustomOAuth2User(localUser.get(), oAuth2User.getAttributes());
        }

        if (memberRepository.findByEmail(email).isPresent()) {
            // 중복된 이메일이 존재할 경우 커스텀 예외를 발생시켜 Spring Security에서 실패로 처리하도록 함
            throw new EmailAlreadyExistsException("이미 사용 중인 이메일입니다.");
        }

        System.out.println("로컬 DB에 Spotify 사용자가 존재하지 않음, 새로운 사용자 등록 시작");

        String displayName = oAuth2User.getAttribute("display_name") != null ? oAuth2User.getAttribute("display_name") : "사용자";
        String nickname = email.split("@")[0];
        createSpotifyUserInLocalDB(email, displayName, nickname, spotifyLoginId);

        Member newUser = memberRepository.findBySpotifyLoginId(spotifyLoginId).orElseThrow();
        return new CustomOAuth2User(newUser, oAuth2User.getAttributes());
    }

이게 해당 로직 전문이다.

암튼 이렇게 신경쓰이던 부분 해결 완료.