사이드 프로젝트 리펙토링(2) JwtFilter + OAuth2 소셜로그인 구현 복기

김정훈·2024년 7월 4일
0

예전에 했던 소셜로그인 부분을 리펙토링 하려다 보니 내용을 까먹어서 복기할 겸 포스팅을 하려고 합니다.

참고 자료

링크 : https://www.youtube.com/playlist?list=PLJkjrxxiBSFALedMwcqDw_BPaJ3qqbWeB

이 분의 강의 자료를 서두에 언급한 이유는 많은 도움이 되었기 때문입니다. 여러 자료를 참고하며 구현을 하다 보면 다양한 블로그 글들을 접하게 되는데, 이 분의 강의를 듣고 나서 다시 블로그 글들을 검토해 보니 잘못된 정보들이 상당히 많다는 것을 알게 되었습니다. 소셜로그인을 구현하는 과정이 힘들었는데 정말 감사하다고 전하고 싶습니다.

기본적인 구조

구현 기법

클라이언트에서 전부 처리, 클라이언트 & 서버 분산 처리, 서버에서 전부 처리 하는 등의 방법들이 있습니다. 우선 잘못된 방법클라이언트 & 서버 분산처리좋은 방법 중의 하나서버에서 전부 처리 하는 방법에 대해 소개하겠습니다.

클라이언트 & 서버 분산처리


이 방법을 이용한 글들이 많았는데요. 이 방법이 잘못된 이유는 바로 클라이언트 -> 서버로 인증 코드를 전송하는 과정이 생긴다는 점입니다. 전송 과정에서 탈취당할 위험이 생길 수 있고, 불필요한 과정이 추가되는 것이기 때문입니다.

서버에서 전부 처리

위와 같이 서버에서 모든 처리를 전담한다면 보안 + 속도 모든 면에서 이점이 있습니다.
참고로 앱은 클라이언트에서 처리하는게 좋습니다.

소셜 로그인 과정

제가 정리한 과정은 다음과 같습니다.

  1. 소셜 로그인 URL 링크로 접속 시도하여 소셜 서버에 로그인 페이지 요청
  2. 사용자에게 로그인 페이지 반환
  3. 로그인 계정 지정하여 로그인 시도
  4. 소셜 서버에서 어플리케이션 서버에 인증 코드 반환
  5. 어플리케이션 서버에서 인증 코드를 이용해 소셜 서버에게 AccessToken 발급 요청
  6. 소셜 서버에서 어플리케이션 서버에 AccessToken 반환
  7. 어플리케이션 서버에서 AccessToken을 이용해 loadUser 실행하여 사용자 정보 획득
  8. 사용자 정보를 정상적으로 받으면 LoginSuccessHandler 실행 (여기서 JWT를 발급하여 클라이언트에 전달)
  9. 클라이언트는 JWT 토큰을 이용해 로그인 시도 및 로그인 성공

여기서 8번 과정을 약간 변형시켰습니다. 바로 서버에서 일회용 인증코드를 발급하고 클라이언트에게 전달한 다음 클라이언트가 곧바로 일반 로그인을 인증 코드와 함께 요청하면 로그인이 되는 것이죠. 이게 가능한 이유는 제가 회원 관리를 소셜 회원, 일반 회원간의 구분을 두지 않았기 때문입니다.

다시 말해

  1. 사용자 정보를 정상적으로 받으면 LoginSuccessHandler 실행 (여기서 일회용 인증 코드를 클라이언트에 전달)
  2. 인증 코드와 함께 일반 로그인 요청 및 성공

이렇게 한 이유는 다음과 같습니다.
1. 일반 로그인 로직의 재사용
2. 응답헤더에 토큰을 넣어 전달하려고 했지만 리다이렉션으로 인한 응답 손실 발생

코드를 일부 보여드리자면

서버

 private static void sendAuthenticationCode(HttpServletRequest request, HttpServletResponse response, String emailId) throws IOException {
        HttpSession session = request.getSession();
        String authenticationCode = UUID.randomUUID().toString();
        session.setAttribute("AuthenticationDto", new AuthenticationDto(emailId,authenticationCode));
        response.sendRedirect("http://localhost:8080/#/oauth/callback?authenticationCode=" + authenticationCode);
    

클라이언트

function Oauth() {
    const location = useLocation();
    const urlParams = new URLSearchParams(location.search);
    const authenticationCode = urlParams.get('authenticationCode');
    const navigate = useNavigate(); // Hook for navigation

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await axios.post("/auth/oauthLogin", {
                    authenticationCode: authenticationCode
                },{
                    withCredentials: true
                });
                localStorage.setItem('accessToken', response.data.accessToken);
                localStorage.setItem('userName', response.data.username);
                localStorage.setItem('mySummonerInfo', JSON.stringify(response.data.summonerInfoDto));
                localStorage.setItem("member", JSON.stringify(response.data.memberDto));

이 땐 쿠키에 대해 잘 알지 못해서 응답헤더를 사용할 생각만 했었고 무엇 보다 일반 로그인을 재사용 하고 싶다는 생각이 컸습니다. 하지만 이렇게 할 경우 일회성 인증 코드를 url에 포함하는 치명적인 보안 문제가 발생합니다. 따라서 쿠키에 인증 코드를 담아 보낸다면 해결이 가능합니다. 여기서 또 다른 방법이 생각나는데요. 바로 직접 토큰을 발급해서 쿠키에 담는 것입니다.

두 방법의 과정은 다음과 같습니다.

  1. 일회용 인증 코드 발급 + 세션에 인증 코드 저장 + 쿠키에 담아 클라이언트에 전달 + 클라이언트에서 인증코드를 이용해 사용자 정보 요청 + 서버는 요청에 따라 세션에서 인증 코드 확인 + 토큰 발행 + 사용자 정보 전달
  2. 토큰 발행 + 쿠키에 담아 클라이언트에 전달 + 클라이언트가 토큰을 이용해 사용자 정보 요청 + 사용자 정보 전달

2번 방법은 대부분 많이 쓰는 방법이기 때문에 2번 방식으로 구조를 변경했습니다.

변경 후

서버

 private void sendToken(HttpServletResponse response, Member member) throws IOException {
        TokenDto tokenDto = jwtService.generateToken(member.getEmailId());
        response.addCookie(createCookie(Token.TokenName.accessToken,tokenDto.getAccessToken()));
        response.sendRedirect("http://localhost:8080/#/oauth/callback");
    }

클라이언트

function Oauth() {
    const [cookie, getCookie] = useCookies(['accessToken']);
    const navigate = useNavigate();

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await axios.post("/auth/oauthLogin", {
                },{
                    withCredentials: true
                });

                localStorage.setItem('accessToken', response.data.accessToken);
                localStorage.setItem('userName', response.data.username);
                localStorage.setItem('mySummonerInfo', JSON.stringify(response.data.summonerInfoDto));
                localStorage.setItem("member", JSON.stringify(response.data.memberDto));

성능 비교

리펙토링 전


시간 : 1.5초

리펙토링 후

시간 : 0.72초

성능이 많이 향상된 것 같다.

profile
백엔드 개발자

0개의 댓글