OAuth2로 로그인 구현 맛보기

풀떼기·2023년 11월 23일

SpringBootLearn

목록 보기
8/8

OAuth

제3의 서비스에 계정 관리를 맡기는 방식.
교재에서는 권한 부여 코드 승인 타입으로 리소스 오너 정보를 취득한다.
교재의 그림과 설명이 묘하게 맞지 않아 이해하기 힘들었다. 결국 구글링에 들어갔다.

(이미지 출처 : https://itwiki.kr/w/OAuth)

  1. 클라이언트가 권한 서버에게 권한 요청
    • 사용자가 로그인함으로써 접근 동의
    • 권한 서버가 클라이언트에게 인증 및 권한 부여 수신
    • redirect_url로 리다이렉션되면서 파라미터에 인증 코드 제공
  2. 인증 코드를 엑세스 토큰으로 교환
    • 로그인 세션에 대한 보안 자격을 증명하는 식별 코드
    • 보통 \token POST 요청을 보낸다
    • 엑세스 토큰 제공
  3. 엑세스 토큰을 사용하여 리소스 서버에 API 호출
    • 정보가 필요할 때마다 API 호출을 통해 정보를 요청
    • 리소스 서버가 토큰의 유효성 검사 후 반환

토큰 발급

구글 클라우드 콘솔을 통해 구글 로그인 기능을 사용해본다.

동의 과정을 마치고 사용자 인증 정보 관리 등 필요한 내용을 체크하고 작성하다보면 OAuth 클라이언트를 생성할 수 있다.
클라이언트 id와 클라이언트 보안 비밀번호를 가지고 구현해보자.

구현

의존성을 추가하고, 가장 먼저 쿠키 관리 클래스를 생성한다.
쿠키 추가, 쿠키 삭제, 직렬화, 역직렬화 메서드를 구현한다.

직렬화, 역직렬화?

  • 직렬화 : 어떤 데이터를 다른 곳에서 사용할 수 있게 다른 포맷의 데이터로 바꾸는 것
  • 역직렬화 : 다른 포맷의 데이터로 바뀐 데이터를 원래 포맷으로 복구하는 것

참고 : https://www.inflearn.com/questions/67208/serialize-desserialize%EC%9D%98-%EB%9C%BB
자바에서의 직렬화 : https://velog.io/@whitebear/자바-직렬화-확실히-알고-가기

Java optional에 대해

교재를 따라 진행하다보니 아래와 같은 코드가 많이 보인다.

User user = userRepository.findByEmail(email)
			.map(entity -> entity.update(name))
            .orElse(User.builder()
            	.email(email)
                .nickname(name)
                .build());

이 코드는 Java의 Optional을 활용한 것으로(map, orElse 등) 정확히 이해하는 데 어려움을 겪고 있어 관련 학습을 한 뒤에 다시 진행하기로 했다.

학습 완료!
위 코드는 userRepository에서 findVyEmail()을 실행 후(Optional<User> 반환), 값이 있으면 entity(User)를 업데이트, 값이 null일 때는 User.build()를 실행해 유저를 생성하는 코드다. 강의를 듣고 보니 이해가 된다!



어렵다!

당연히 한 번에 이해하려는 생각은 없었고, 어떻게 동작하는지 맛보기로 휘리릭 볼 생각이었는데 신경쓸게 정말 많다. 책으로는 확실히 한정된 정보만 보게되니까 앞뒤 맥락을 이해하려 소비되는 시간도 많다. 기본적으로 방대한 내용이라는 건 알고 있었지만, 직접 타이핑해보니 바로 이해가 되지는 않는다. 그림으로 정리해보는 것도 나쁘지 않을 것 같다.


문제 : 액세스 토큰이 삭제되었을 경우

리프레시 토큰을 만들어 사용하고 있으므로 정상적인 흐름이라면 /api/token API를 호출해 새 액세스 토큰을 발급받아야한다.
그런데 의도적으로 액세스토큰을 삭제했을 때, API가 호출되지 않았다. 인증 실패 전에 인증 단계에서 재발급을 하려면 어떻게 해야할까?
백엔드단에서 삽질을 하다가 js 파일을 살펴보니... 여기서 해결해야겠다는 결론이 났다.

// HTTP 요청을 보내는 함수
function httpRequest(method, url, body, success, fail) {
    fetch(url, {
        method: method,
        headers: {  // 로컬 스토리지에서 엑세스 토큰 값을 가져와 헤더에 추가
            Authorization: 'Bearer' + localStorage.getItem('access_token'),
            'Content-Type': 'application/json',
        },
        body: body,
    }).then((response) => {
        if(response.status === 200 || response.status === 201) {
        return success();
    }
    const refresh_token = getCookie('refresh_token');
    if(response.status === 401 && refresh_token) {
        fetch('/api/token', {
            method: 'POST',
            headers: {
                Authorization: 'Bearer ' + localStorage.getItem('access_token'),
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                refreshToken: getCookie('refresh_token'),
            }),
        })
            .then((res) => {
                if(res.ok) {
                    return res.json();
                }
            })
            .then((result) => {   // 재발급이 성공하면 로컬 스토리지값을 새로운 액세스 토큰으로 교체
                localStorage.setItem('access_token', result.accessToken);
                httpRequest(method, url, body, success, fail);
            })
            .catch(error => fail());
    } else {
        return fail();
    }
    });
}

기존에 작성해둔 코드에 이미 401에러가 났을 때 /api/token을 호출하는 로직이 있었다!
따라치면서 잘 이해하지 못했던 게 화근이다. 이걸로 몇시간을 고민했다.
근데 왜 작동을 하지 않은거지? 리프레시 토큰도 제대로 있는데?
console.log()를 찍어보니...

찾을 수 없다. 설마설마... getCookie 함수를 다시 살펴봤다.

// 쿠키를 가져오는 함수
function getCookie(key) {
    var result = null;
    var cookie = document.cookie.split(";");
    cookie.some(function(item) {
        item = item.replace(" ", "");

        var dic = item.split("=");

        if(key === dic[0]) {
            result = dic[1];
            return true;
        }
    });

}

그렇다. result를 return해주지 않고 있었다. 이런 바보같은 실수는 언제쯤 안하려는지!
당연하게도 return값을 추가해주니 정상적으로 액세스 토큰을 발급받았다.
문제 해결!😂

profile
주니어 백엔드 개발자입니다.

0개의 댓글