Coding History/project

OAuth2 로그인 후, 세션 관리. (axios를 사용해 헤더로 토큰 전송)->인 줄 알았으나 결론은 Google OAuth2 로그인 간소화

BlackBirdIT 2024. 9. 17. 16:21

세션 관리와 로그아웃으로 넘어가면 된다!

일단 세션 관리는 잘 된다.

                // 세션 관리 설정
                // OAuth2 로그인 설정
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/usr/member/login")
                        .successHandler(oAuth2AuthenticationSuccessHandler) // 로그인 성공 후 핸들러
                        .defaultSuccessUrl("/usr/home/main", true)
                        .failureUrl("/usr/member/login?error=true")
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(customOAuth2UserService))
                )
                // 세션 상태 관리 (JWT는 stateless, OAuth2는 세션 사용)
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)  // OAuth2는 세션 사용
                )
                // JWT 필터를 추가하여 토큰 인증만 처리
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .headers(headersConfigurer ->
                        headersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
                )

시큐리티 코드를 이렇게 고치고 로그인 하니까

이렇게 사용자가 이제 뜬다.

근데 문제가 좀 생겼는데 만들었던 로그아웃 버튼이 실행이 안된다. 콘솔을 보니까.

로그인 사용자를 가져오는데 로그인 되지 않았다고 해서 로그아웃이 안되는 것 같다. 뭔소리일까 이건 또..

엄 일단 알아보니까 로그인 할 때, 내가 사용하지 않던 페이지로 리디렉션 했던 것 처럼 로그아웃도 "https://accounts.google.com/Logout?continue=http://localhost:8081/usr/member/login"여기로 리디렉션 해줘야되네.

// Firebase 로그아웃 + Google OAuth2 로그아웃 처리
function socialLogout() {
    const auth = getAuth(); // Firebase Auth 인스턴스 가져오기
    auth.signOut()
        .then(() => {
            console.log("Firebase 로그아웃 성공");
            // 서버에서 세션 무효화 처리
            return fetch('/doLogout', {
                method: 'POST',
            });
        })
        .then(response => {
            if (response.ok) {
                console.log("서버 로그아웃 성공");
                // Google 로그아웃 페이지로 리다이렉션
                window.location.href = "https://accounts.google.com/Logout?continue=http://localhost:8081/usr/member/login";
            } else {
                console.error("로그아웃 실패: ", response.status);
            }
        })
        .catch(error => {
            console.error("로그아웃 에러 발생: ", error);
        });
}

하고 시도를 해보니까.

Firebase 로그아웃 성공까지는 뜨는데 OAuth2에서 문제가 생기는걸 발견.

Firebase 로그아웃을 먼저 처리한 후, 서버 로그아웃을 처리하고, 마지막에 Google OAuth2 로그아웃을 클라이언트에서 직접 처리.
서버에서는 세션 무효화만 담당하고, 클라이언트에서 Google 로그아웃을 처리하도록 수정.

해서 로그아웃은 일단 시켰는데 내 브라우저 모든 구글 로그인이 다 로그아웃되네.. 이건 일단 나중에 수정하고.

다시 로그인해서 JWT를 제대로 처리하고 있는지 다시 확인해보니까 아닌 것 같다.

알아보니까 클라이언트에서 JWT 토큰을 헤더에 추가해서 보내야된다고 하는데 확실히 난 이거 한 적 없으니까 일단 이거부터 해야될듯?

알아보니까 형식이 두가지가 있는데 기본형인 fetch와 axios가 있다. axios는 따로 설치가 필요하고, 또 사용이 편리하고 여러 라이브러리가 존재한다고 하는데 음.. 좀 고민이네.

npm install axios

뭐 설치도 간단한 것 같은데 이거 써볼래.

설치를 하니까 취약점이 발견되어서

npm audit fix

추가로 해당 명령어까지 실행시켜서 모두 해결 했다.

그리고 index.js에서 fetch로 사용하던 것을 axios로 바꿔주고, Authorization헤더로 실어보내는 코드를 약간 추가해주고 로그인 시도를 하니까.

JWT까지 로그에 찍히는 것을 확인했다!


지금 한 세시간 뻘짓을 했는데 일단 명확하게 이유는 찾았다 .

지금 login.jsp내에

    <script>
        // 구글 OAuth2 로그인 처리 (Spring Security를 통해 처리)
        function googleLogin() {
            // Spring Security의 OAuth2 로그인 엔드포인트로 리다이렉트
            window.location.href = "/oauth2/authorization/google";
        }
    </script>

이 스크립트 코드가 존재 했는데 이거 때문에 내가 index.js에서 설정한 토큰 전달 로직 자체가 실행되지 않았던 것. 왜? 이 함수를 실행시키고 로그인 로직을 사용하고 있었으니까..

그래서 이걸 지우고 콘솔을 보니까..

그제서야 토큰을 생성했다.. 드디어 다음 단계로 갈 수 있겠네 어휴 진짜 딴거만 계속 들여다봤네.

근데 지금 서버 응답이 로그인 페이지로 리다이렉트 한다는 것이 문제다. 그럼 내가 만든 서비스나 시큐리티에 문제가 있을 요지가 있으니까 그쪽 코드를 살펴보자.

저번에 지웠던 firebase service와 컨트롤러를 다시 만들어서

이렇게 성공된 결과를 얻었다. 이제 또 방금 만든 로직과 userDetailService 두개가 유기적으로 엮기게 만들면 완성이다.

그래서 좀 고쳐보고 시도해보니까 콘솔에

흠.. 여기서부터 걸리네 헤더에 실어나르는 것도 문제가 없는데 어디가 문제일까

그래서 로그를 더 찍어볼 수 있게 했다. Authorization 헤더에서 가져온 토큰을 찍어보는 로직을 추가 했는데,

가져오는데 비어있다고 하네 뭐지?

그래서 로그 단계를 조금 더 높혀봤다

private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        System.out.println("Authorization 헤더: " + bearerToken); // 추가된 로깅
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
private String getJwtFromRequest(HttpServletRequest request) {
    String bearerToken = request.getHeader("Authorization");
    System.out.println("Authorization 헤더: " + bearerToken); // Authorization 헤더 출력

    if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
        String token = bearerToken.substring(7);
        System.out.println("추출한 JWT 토큰: " + token); // Bearer 이후의 토큰 출력
        return token; // Bearer 이후의 토큰만 반환
    } else {
        System.out.println("JWT 토큰이 유효하지 않거나 없습니다.");
    }
    return null;
}

설명하자면 가져온 토큰값을 가공하는 단계다. 위의 이미치처럼 Bearer로 시작하는 문구로 가져오니 토큰 값만 추출하기 위해서 substring(7);을 사용해서 가공후 전달하는 방식인데 여기서 문제가 있을 요지가 있으니까 더 로그를 추가해봤다.

했는데 값은 잘 가져오는데 이러네..

그러다 문득 처음 JWT 할 때 환경변수 등록했던 것이 기억났다. 그래서 그 설정을 지우고 왔다.

지우니까 서버가 아예 안켜져서 알아보니까 googleJWT만 사용하면 애초에 필요없었던 걸 내가 만들고 있었다;;;

그래도 공부는 됐으니까 후회는 하지 않는데 좀 시간이 아깝긴 하네.... 이걸 구글이랑 연동되게 고쳐서 쓰자.

그래서 구글 JWT를 사용하기 위해서 코드 클래스 두개를 가져왔고,

이런식으로 빨간 오류가 좀 많아서 알아보니까 새로운 의존성이 또 필요했다.

        <!-- java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.1</version>
        </dependency>

이렇게 추가해줬다.

이러고 이제 기본 설정이 되는 코드들을 죄다 긁어왔다.

이렇게 있고, 순서대로 짧게 요약하자면

  • FirebaseTokenVerifier: Firebase 토큰 검증
  • GoogleAuthenticationToken: 인증된 사용자 정보를 Spring Security에 저장하는 객체
  • GoogleJwtAuthenticationFilter: JWT 토큰을 필터링하여 검증하는 필터
  • GoogleJwtVerifier: Google JWT 토큰을 검증하는 클래스
  • GooglePublicKeyProvider: Google에서 제공하는 공개 키를 가져오는 클래스
  • JwtHelper: JWT에서 kid 값을 추출하는 유틸리티 클래스

이렇다. 솔직히 뭔지 잘 모르겠고 오류 안나게 처리하고, 내가 가진 고유의 값을 찾아 넣는것에 조금 애를 먹었다.

그래서 만들어둔 다른 코드들도 이제 구글 JWT인증 방식을 도입하면서 전면 수정에 나섰다.

OAuth2AuthenticationSuccessHandler클래스에서 기존 JWT 인증 방식 자체를 없애고 기능만 돌아가도록 수정했고 (구글에서 받아와서 검증할 필요가 없어짐) CustomOAuth2UserService에서는 기존 JWT 방식이 아닌 새로 만든 구글 JWT로 내 DB에 접근할 수 있도록 로직을 수정했다.

이후 모든 빨간줄이 사라져서 서버를 켜봤는데...

이런 에러가 떴다.

뭔지 알아보니까, '순환참조'에러라고 하는데, 내 문제 상황에서의 순환 참조 에러는 이렇다.

SecurityConfig에서 CustomOAuth2UserService를 의존성으로 주입하고 있고, CustomOAuth2UserService에서 다른 빈을 의존성으로 주입하면서 순환 참조가 발생하고 있다.

그럼 순환참조가 뭘까?

순환 참조

순환 참조란 두 개 이상의 객체(클래스, 빈)가 서로를 참조하는 상황을 말한다. 쉽게 말해, A 객체가 B 객체를 필요로 하고, 동시에 B 객체가 A 객체를 필요로 하는 상태다. 이것은 아래와 같은 예시로 설명할 수 있다:

  • A 클래스가 B 클래스를 사용하고,
  • B 클래스가 다시 A 클래스를 사용하려고 하면,
    스프링 같은 프레임워크에서 의존성 주입을 할 때, 어떤 객체를 먼저 생성해야 할지 결정하기 어려워져서 무한 루프에 빠질 수 있다.

그러니까 SecurityConfigCustomOAuth2UserService에서 이 문제가 발생한 것.

해결 방법은 찾아보니까 의외로 간단했다 @Lazy어노테이션이면 해결된다. 너무 간단해서 올바른 해결법이 맞는가 좀 알아봤는데 순환참조를 해결하기 위해서 만든 코드라고 한다. 뭐 일단 고민 크게 하지 말고 써보자.

SecurityConfig

    @Autowired
    @Lazy  // 순환 참조 방지를 위해 @Lazy 적용
    private CustomOAuth2UserService customOAuth2UserService;

이렇게 어노테이션을 부여했다.

이렇게 한줄로 서버가 켜지다니...

이제 테스트 해보자!

오 드디어 뭔가 바뀌었다.

브라우저 콘솔에서는 아까 아무문제 없었던게 문제가 생겼다.

로그 단계를 더 높혀서 콘솔을 보니까

JWT Verification Failed: java.security.InvalidKeyException: IOException: DerValue.getOID, not an OID -96
이런 에러가 떴는데.

JWT 서명 검증 과정에서 잘못된 키 형식이 사용되었을 때 발생할 수 있는 문제라고 한다.

방금 이 에러 찾다가 내가 또 뻘짓을 했다는 것을 알았다.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

해당 의존성 추가하고,

          jwt:
            issuer-uri: https://accounts.google.com

해당 코드 yml에 추가해주면 시큐리티에서 알아서 인증해준댄다...

그래도 인증 흐름을 코드를 보면서 약간은 익혔고, 순환 참조라는 새로운 개념도 알게 되었으니 헛된 시간은 아니였겠지.

일단 과감히 아까 만들었던 JWT관련 클래스를 다 지워버렸다.

CustomOAuth2UserService클래스도 JWT 관련 코드 전부 지우고 OAuth2 인증만 집중할 수 있도록 수정했다.

이후 서버 재시작으로 오류 없이 서버가 돌아가는 것을 확인하고 이제 다시 로그인 시도.

또 브라우저에서만 성공하고 백엔드로는 가지 않는 모습.

하다가 저번에 로그인 버튼 두개여서 문제가 생겼던게 생각나서 git 에서 해당 링크가 걸려있는 버튼 다시 가져왔다.

// Google 로그인 함수 (전역 스코프에 저장된 객체를 참조)
function googleLogin() {
    console.log('%c[INFO] Google 로그인 시도 중...', 'color: blue');
    signInWithPopup(auth, provider)
        .then((result) => {
            console.log('%c[INFO] Google 로그인 성공:', 'color: green', result.user);
            checkAuthState();  // 로그인 후 인증 상태 재확인
        })
        .catch((error) => {
            console.error('%c[ERROR] Google 로그인 실패:', 'color: red', error);
        });
}

// googleLogin 함수를 전역 스코프에 노출시킴
window.googleLogin = googleLogin;

function googleLogin() {
            // Spring Security의 OAuth2 로그인 엔드포인트로 리다이렉트
            window.location.href = "/oauth2/authorization/google";
        }

아래걸로 로그인 시도했을 때는 백엔드로 접근을 했었다.

내가 보기에는 이 두가지 방식이 혼합되어야될 것 같다는 생각에 도달..

이여서 어떻게 합쳐야될지 몰라서 gpt한테 물어보니까 그냥 아래꺼 쓰랜다.

그래서 그냥 아래것만 쓰게끔 index.js에서 처리하니까 완벽하게 로그인 성공!!

세션 유지도 잘 된다.

이렇게 간단한걸 쓸데없는 사족 계속 붙히면서 멀리도 돌아왔구나.

일단 구글 로그인 결론.

Google OAuth2를 Spring Security에서만 처리하는 방식사용. Google JWT는 시큐리티에서 자동으로 처리하는 방식 사용.

Firebase SDK는 사용하지 않고, Spring Security의 OAuth2 엔드포인트로 바로 리다이렉트하는 방식.

Firebase SDK인증을 무조건 받아야된다고 착각했다. Storege를 사용하니까 이게 있어야지 되는 줄 알고 계속 집착했는데 진작 제대로 알아보고 했으면 구글 OAuth2를 사용해도 문제 없었다는 것을 알 수 있었을 것...

그래도 좋은 경험이 됐던 것 같다. 구글로그인은 이제 진짜 더이상 손 볼 일이 없었으면 좋겠다.. 어찌됐든 간에 뿌듯하다.

이젠 진짜 Spotify API를 사용한 로그인과 플레이어 구축으로 들어갈 예정. 이후엔 유저가 파일 업 다운로드 할 수 있게 구축하면 거의 끝이다.