개요

⚡️ 개발환경

  • Front - React.js
  • Back - SpringFramework(legacy) 4.3.30
  • DB - MySql5.7
  • Server - Tomcat 9
  • Tool - 전자정부프레임워크 3.0(수업목적), VScode
  • Build - maven

⚡️ 시나리오

  1. React(FE)를 통한 API 호출(redirect)
  2. kakao & naver 로그인 버튼 클릭 시 콜백함수 호출
  3. 콜백함수 호출과 동시에 axios를 통하여 spring 서버로 비동기 요청
  4. spring에서 넘겨받은 인증코드를 통해 각 소셜로그인 토큰 발급
  5. 발급된 소셜 로그인 토큰에서 유저 정보 획득
  6. 획득된 유저정보를 분석하여 새로운 유저는 DB 저장 후 jwt발급, 기존 유저는 jwt 발급
  7. spring에서 발급된 jwt를 React(FE)에서 localsotorage에 저장

위와 같은 시나리오를 가지고 시작하기 위해 그림을 먼저 그려보았다.

⚡️ 소셜로그인 + JWT 구현 이유

인증방식을 구현할 때 사용자가 서버를 신뢰할 수 없는 경우도 있고, 서버는 사용자의 민감한 정보를 관리하는 것에 대한 부담을 가질 수 있다. 이러한 부분에서 어떻게 보다 안전하게 로그인 시 정보를 획득하여 로그인을 유지할 수 있는지에 대해 생각해보았다.(개인프로젝트 주제에....🤔)
그러다가 소셜 로그인을 사용한 로그인 방법을 생각하게 되었고, 여기서 획득한 정보를 가지고 서버내에서 JWT를 발급해준다면 어떨까? 하는 생각이 들었다.

우선 OAuth는 토큰 내에 정보가 많이 들어있다. 이 말은 OAuth는 필요한 정보를 획득하는데 모호한 상태로 유지될 수 있다는 점이다. 이러한 부분을 보완하기 위해서 JWT토큰을 이용해 필요한 정보만을 OAuth에서 추출하여 보관한다면 로그인을 완료한 유저가 다른 서비스를 사용할 때 서버는 유저의 필요한 정보만을 확인할 수 있을 것이다.

그리고 개인적으로는 소셜로그인을 통해 받아온 정보를 가공해서 일반 로그인을 사용하는 유저들과 동일하게 JWT를 발급함으로써 서비스의 일관성을 유지하고 싶었다.


기능구현

⚡️ API 준비

사실 API 준비는 공식 문서를 보는 것이 가장 정확한 것 같다. 구글링을 통해 획득된 정보들은 사용법이 일부 차이가 날 수 있고, 개발환경에 따라 에러가 빈번하게 발생할 수 있다. 그러므로 아래 사이트를 기본으로 참고해서 시작하자.

카카오 : https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
네이버 : https://developers.naver.com/docs/login/devguide/devguide.md#%EB%84%A4%EC%9D%B4%EB%B2%84%20%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EA%B0%80%EC%9D%B4%EB%93%9C

⚡️ FE(React)

나는 FE를 react-app으로 생성하여 localhost:3000을 사용하고 있다. 각각의 소셜 로그인 버튼을 커스텀하려 했지만 로직을 구현하는 것에 더욱 집중을 하고 싶어서 각 공식문서에서 사용하도록 권장하는 이미지를 이용하여 클릭 시 url로 이동하도록 구현하였다.

🖥️ Kakao Login 페이지 이동(KakaoLogin.jsx)

function KakaoLogin() {
  // 보안상 노출되면 안되는 데이터는 .env에 작성하여 호출하였다.
  const client_id = process.env.REACT_APP_KAKAO_CLIENT_ID;
  const redirect_uri = process.env.REACT_APP_KAKAO_REDIRECT_URI;

  const url = `https://kauth.kakao.com/oauth/authorize?scope=account_email&client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code&prompt=login`;

  //cors 이슈로 인해 href 방식으로 호출
  const loginKaKao = () => {
    window.location.href = url;
  }
  return <SNSLink img="./img/login/kakao.png" onClick={loginKaKao} />
}

export default KakaoLogin;

🖥️ Naver Login 페이지 이동(NaverLogin.jsx)

function NaverLogin() {
  // .env 작성
  const client_id = process.env.REACT_APP_NAVER_CLIENT_ID;
  const redirect_uri = process.env.REACT_APP_NAVER_REDIRECT_URI;
  const state = process.env.REACT_APP_NAVER_STATE;

  const url = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${client_id}&state=${state}&redirect_uri=${redirect_uri}`;

  //cors 이슈로 인해 href 방식으로 호출
  const loginKaKao = () => {
    window.location.href = url;
  }
  return <SNSLink img="./img/login/naver.png" onClick={loginKaKao} />
}

export default NaverLogin;

각각의 로그인 페이지는 https://kauth.kakao.com/oauth/authorize url이 ajax 통신에 대한 오류가 지속적으로 발생하여 redirectUrl을 통한 code의 반환값을 받기 위해 href를 이용하여 페이지를 전환하였다.

🖥️ Kakao Callback(KakaoCallback.jsx)

function KakaoCallback() {
  //최초 렌더링 시 발동
  useEffect(() => {
    const code = new URL(window.location.href).searchParams.get("code");
    
    //spring 서버로 인증키를 통해 유저정보를 획득하고 로그인 처리 요청
    axios.post('/api/client/login/oauth/kakao', {
      authorizationCode: code
    }).then((response) => {
      
        //spring에서 발급된 jwt localStorage 저장
        localStorage.setItem("accessToken", response.headers.accesstoken);
      
        //메인 페이지로 이동
        window.location.href = "/";
      }).catch((err) => {
        //에러발생 시 경고처리 후 login 페이지로 전환
        alert(err.response.data.detail);
      
        window.location.href = "/login";
      })
  }, []);

  return (
    <div>
      <Loading />
    </div>
  )
}

export default KakaoCallback;

🖥️ Naver Callback(NaverCallback.jsx)

function NaverCallback() {
  useEffect(() => {
    const code = new URL(window.location.href).searchParams.get("code");
    const state = new URL(window.location.href).searchParams.get("state");
    //console.log(code);
    //console.log(state);

    axios.post('/api/client/login/oauth/naver', {
      authorizationCode: code,
      state: state
    }).then((response) => {
      //spring에서 발급된 jwt 반환 localStorage 저장
      localStorage.setItem("accessToken", response.headers.accesstoken);

      //메인 페이지로 이동
      window.location.href = "/";
    }).catch((err) => {
      //에러발생 시 경고처리 후 login 페이지로 전환
      alert(err.response.data.detail);
      window.location.href = "/login";
    })
  }, []);

  return <Loading />
}

export default NaverCallback;

redirectUrl을 통해 각 API별 Callback을 호출한다. 이 시점에 spring으로 인가코드를 전달하여 이후 로직을 처리한다.

여기서 의문은 href를 이용하지 않고 FE에서 토큰까지 발급을 받고난 다음 spring 서버로 다음 로직을 전달할 수 있을텐데, 이렇게 하는 것이 보안에 취약하지는 않은 것인지 의문이 살짝 들었다. 나는 최대한 서버에서 모든 요청을 수행하고 최종적으로 JWT만 반환하고 싶어서 인가코드만 FE에서 반환받아 서버로 전달하였다.

(보완할 점이 혹시 있다면 이야기해주시면 감사드리겠습니다!)

FE는 이렇게 기능구현이 끝났다. 로그인이 승인되어 완료되면 JWT가 localStorage에 저장이 되고, 이후 서비스 사용 시 해당 JWT를 통해 서버에서 인가를 받으면 될 것이다.

⚡️ BE(Spring)

프론트에서 넘겨받은 인가코드를 통해 서버에서는 토큰발급을 요청하고, 토큰을 통해 유저정보를 요청하며, 획득된 유저정보를 통해 DB에 신규회원 등록 및 JWT를 발급할 것이다.

간단하게 spring에서 사용할 객체들에 대해서 소개하고 시작하자.

  • LoginController : 일반 및 소셜로그인 요청을 담당할 컨트롤러
  • OAuthService : Oauth에 대한 전반적인 비즈니스 로직을 수행할 서비스
  • RequestOauthInfoService : 카카오 또는 네이버 API요청을 구분하고, 인증된 유저정보를 반환해줄 객체
  • OauthClient : 카카오 및 네이버 API의 직접적인 통신을 담당할 객체가 구현할 interface
  • OauthMember : 각 API의 유저정보와 매핑할 객체들이 구현할 interface
  • OauthParams : API 통신 시 객체 주입의 판단근거로 사용할 파라미터 객체의 interface
  • OauthProvider : 열거형 클래스로 API 요청 대상을 판단하기 위해 선언한 객체

대표적으로 로직 구현에 사용한 객체들이다. 위의 객체들을 구현할 하위 객체들은 아래 코드에서 확인해보자.

🖥️ OauthProvider.java

package com.edu.surfing.domain.oauth;

// 로그인 타입을 구분하는 열거형 클래스
public enum OauthProvider {
	KAKAO, NAVER
}

차후 Map의 키값으로 사용할 열거형 클래스이다. 나중에 사용하는 시점에서 알아보자.

🖥️ OauthParams.java

public interface OauthParams {
	public OauthProvider oauthProvider();
	public String getAuthorizationCode();
	public MultiValueMap<String, String> makeBody();
}

열거형 클래스의 자료형을 보유하고, 각 API의 인가코드와 Http 통신에 필요한 바디를 생성하는 메소드를 보유한 interface이다. 아래에서 구현한모습을 보자.

🖥️ KakaoParams.java

@Getter
public class KakaoParams implements OauthParams {
	// Controller에서 Post요청으로 전달된 파라미터
	private String authorizationCode;

	@Override
	public OauthProvider oauthProvider() {
		return OauthProvider.KAKAO; // Enum 자료형 지정
	}

	@Override
	public MultiValueMap<String, String> makeBody() {
		// 필수로 포함되어야할 Body 작성
		MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
		body.add("code", authorizationCode);
		return body;
	}

	@Override
	public String getAuthorizationCode() {
		return authorizationCode;
	}
}

🖥️ NaverParams.java

@Getter
public class NaverParams implements OauthParams{
	// Post 요청 시 파라미터로 전달
	private String authorizationCode;
	private String state;
	
	@Override
	public OauthProvider oauthProvider() {
		return OauthProvider.NAVER; // Enum 자료형 지정
	}
	
	@Override
	public MultiValueMap<String, String> makeBody() {
		// Body 지정
		MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
		body.add("code", authorizationCode);
		body.add("state", state);
		return body;
	}
	
	@Override
	public String getAuthorizationCode() {
		return authorizationCode;
	}
}

위 객체들은 Controller에서 접수한 Url에 알맞게 객체를 주입하기 위한 파라미터로서 활용될 객체들이다. 요청을 접수한 컨트롤러가 해당 파라미터를 Service의 메소드에 전달하여 줄 예정이다.

이제 Enum과 Params를 선언하여 객체 주입을 판단할 근거를 준비하였으니 로직을 구현해보자.

🖥️ KakaoToken.java

@Data
public class KakaoToken {
	private String token_type;
	private String access_token;
	private String refresh_token;
	private String id_token;
	private int expires_in;
	private int refresh_token_expires_in;
	private String scope;
}

🖥️ NaverToken.java

@Data
public class NaverToken {
	private String token_type;
	private String access_token;
	private String refresh_token;
	private int expires_in;
}

각 API별로 토큰 발급 시 응답해주는 데이터가 다르기 때문에 API의 응답 데이터 형태와 일치하는 Token 객체를 생성해준다.

🖥️ OauthMember.java

// Oauth 로그인을 통한 멤버들이 구현할 인터페이스
public interface OauthMember {
	public String getEmail();
	public String getNickName();
	OauthProvider getOauthProvider();
}

Oauth를 통해 로그인을 해야하는 유저들의 정보를 매핑할 객체들이 구현할 최상위 객체이다. 해당 객체를 구현함으로서 하위 객체들을 동일한 자료형으로 요청에 따라 생성이 가능해진다.

🖥️ KakaoMember.java

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoMember implements OauthMember{
	
	@JsonProperty("kakao_account") // 응답 정보와 동일한 이름의 property 매핑
	private KakaoAccount kakao_account; // response와 데이터 매핑을 위한 _사용
	
	//데이터 반환값을 받을 내장클래스
	//필요한 값만 추출하기 위해서 @JsonIgnoreProperties 사용
	@Getter
	@JsonIgnoreProperties(ignoreUnknown = true)
	public class KakaoAccount{
		private Profile profile;
		private String email;
		
		@Getter
		@JsonIgnoreProperties(ignoreUnknown = true)
		public class Profile{
			private String nickname;
		}
	}

	@Override
	public String getEmail() {
		return kakao_account.email;
	}

	@Override
	public String getNickName() {
		return kakao_account.profile.nickname;
	}

	@Override
	public OauthProvider getOauthProvider() {
		return OauthProvider.KAKAO;
	}
}

🖥️ NaverMember.java

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public class NaverMember implements OauthMember{
	
	@JsonProperty("response") // Naver 응답정보 객체명과 동일
	private Response response; // response와 데이터 매핑을 위한 _사용
	
	//데이터 반환값을 받을 내장클래스
	//필요한 값만 추출하기 위해서 @JsonIgnoreProperties 사용
	@Getter
	@JsonIgnoreProperties(ignoreUnknown = true)
	public class Response{
		private String email;
		private String name;
	}

	@Override
	public String getEmail() {
		return response.email;
	}

	@Override
	public String getNickName() {
		return response.name;
	}

	@Override
	public OauthProvider getOauthProvider() {
		return OauthProvider.NAVER;
	}
}

각 API의 유저정보를 반환받아 데이터를 저장할 객체들이다. 해당 객체들의 정보는 공식문서의 응답 형태를 보면서 매핑하면된다. 이렇게 토큰과 유저의 데이터를 담을 수 있는 객체들이 준비되었으니 요청을 시도해보자.

🖥️

// 로그인 형태에 따른 동작을 위한 인터 페이스
public interface OauthClient {
	public OauthProvider oauthProvider();
	public String getOauthLoginToken(OauthParams oauthParams);
	public OauthMember getMemberInfo(String accessToken);
}

API와 통신을 위한 비즈니스 로직을 수행할 객체들이 구현할 interface이다. 해당 interface의 자료형으로 요청에 동일한 객체를 주입할 예정이다. 아래 구현한 객체들의 코드를 보자.

🖥️ KakaoClient.java

// 카카오 로그인의 토큰 발급 및 유저정보 요청 객체
@Slf4j
@Component
public class KakaoClient implements OauthClient{
	private static final String GRANT_TYPE = "authorization_code";
	
	@Value("${kakao.auth.token.url}")
	private String token_url;
	@Value("${kakao.auth.user.url}")
	private String user_url;
	@Value("${kakao.client.id}")
	private String client_id;
	@Value("${kakao.redirect.uri}")
	private String redirect_uri;

	@Override
	public OauthProvider oauthProvider() {
		return OauthProvider.KAKAO;
	}

	@Override
	public String getOauthLoginToken(OauthParams oauthParams) {
		String url = token_url;
		log.debug("전달할 code:: " + oauthParams.getAuthorizationCode());

		RestTemplate rt = new RestTemplate();
		// 헤더 생성
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

		// 바디 생성
		MultiValueMap<String, String> body = oauthParams.makeBody();
		body.add("grant_type", GRANT_TYPE);
		body.add("client_id", client_id);
		body.add("redirect_uri", redirect_uri);

		// 헤더와 바디 합체
		HttpEntity<MultiValueMap<String, String>> tokenRequest = new HttpEntity(body, headers);
		log.debug("현재 httpEntity 상태:: " + tokenRequest);

		// 토큰 수신
		KakaoToken accessToken = rt.postForObject(url, tokenRequest, KakaoToken.class);
		log.debug("accessToken :: " + accessToken);

		return accessToken.getAccess_token();
	}

	@Override
	public OauthMember getMemberInfo(String accessToken) {
		String url = user_url;
		
		log.debug("넘어온 토큰은:: " + accessToken);

		// 요청 객체 생성
		RestTemplate rt = new RestTemplate();

		// 헤더 생성
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		headers.add("Authorization", "Bearer " + accessToken); // accessToken 정보 전달
		
		// 바디 생성
		MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
		body.add("property_keys",  "[\"kakao_account.email\", \"kakao_account.profile\"]");
		
		// 헤더 + 바디 조합
		HttpEntity<MultiValueMap<String, String>> infoRequest = new HttpEntity<>(body, headers);
		
		//요청 반환데이터를 메소드 리턴값으로 반환
		return rt.postForObject(url, infoRequest, KakaoMember.class);
	}
}

🖥️ NaverClient.java

// 네이버 로그인의 토큰 발급 및 유저정보 요청 객체
@Slf4j
@Component
public class NaverClient implements OauthClient{
	private static final String GRANT_TYPE = "authorization_code";
	
	@Value("${naver.client.id}")
	private String client_id;
	@Value("${naver.client.secret}")
	private String client_secret;
	@Value("${naver.auth.token.url}")
	private String token_url;
	@Value("${naver.auth.user.url}")
	private String user_url;

	@Override
	public OauthProvider oauthProvider() {
		return OauthProvider.NAVER;
	}

	@Override
	public String getOauthLoginToken(OauthParams oauthParams) {
		String url = token_url;
		log.debug("전달할 code:: " + oauthParams.getAuthorizationCode());

		RestTemplate rt = new RestTemplate();
		// 헤더 생성
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

		// 바디 생성
		MultiValueMap<String, String> body = oauthParams.makeBody();
		body.add("grant_type", GRANT_TYPE);
		body.add("client_id", client_id);
		body.add("client_secret", client_secret);

		// 헤더와 바디 합체
		HttpEntity<MultiValueMap<String, String>> tokenRequest = new HttpEntity(body, headers);
		log.debug("현재 httpEntity 상태:: " + tokenRequest);

		// 토큰 수신
		KakaoToken accessToken = rt.postForObject(url, tokenRequest, KakaoToken.class);
		log.debug("accessToken :: " + accessToken);

		return accessToken.getAccess_token();
	}

	@Override
	public OauthMember getMemberInfo(String accessToken) {
		String url = user_url;
		
		log.debug("넘어온 토큰은:: " + accessToken);

		// 요청 객체 생성
		RestTemplate rt = new RestTemplate();

		// 헤더 생성
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		headers.add("Authorization", "Bearer " + accessToken); // accessToken 정보 전달
		
		// 바디 생성
		MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
		
		// 헤더 + 바디 조합
		HttpEntity<MultiValueMap<String, String>> infoRequest = new HttpEntity<>(body, headers);
		
		//요청 반환데이터를 메소드 리턴값으로 반환
		return rt.postForObject(url, infoRequest, NaverMember.class);
	}
	
}

두 객체의 API통신 로직은 거의 유사하다. 하지만 요청 시 헤더의 정보나 바디의 정보가 다르기 때문에 구분하여 선언을해주고, 마지막 getMemberInfo()의 리턴값은 해당하는 API의 OauthMember의 하위 클래스 자료형을 반환해주면 된다.

그렇다면 이렇게 나뉜 OauthClient 객체를 어떻게 구분할 수 있는 지 다음 코드를 보자.

🖥️ RequestOauthInfoService.java

@Component
public class RequestOauthInfoService {
	//Enum = 키, Client = 값으로 저장하는 Map 생성
	private final Map<OauthProvider, OauthClient> clients;
	
	//생성과 동시에 클라이언트 주입
	public RequestOauthInfoService(List<OauthClient> clients) {
		this.clients = clients.stream().collect(
				Collectors.toUnmodifiableMap(OauthClient::oauthProvider, Function.identity()));
	}
	
	//넘겨받은 params의 enum 클래스와 동일한 객체를 주입
	public OauthMember request(OauthParams oauthParams) {
		OauthClient client = clients.get(oauthParams.oauthProvider());
		String accessToken = client.getOauthLoginToken(oauthParams);
		
		return client.getMemberInfo(accessToken);
	}
}

위 객체를 통해 요청한 client가 누군인지 분석을 할 것이다. 먼저 Enum을 키값으로 가지는 Map을 선언한다. 생성자와 동시에 OauthClient 자료형의 객체를 주입받기 위하여 파라미터로 전달해준다.
여기서 stream().collect()에 대해 설명하면. stream한 데이터를 변형하여 원하는 자료형으로 변환해주는 것이다. 여기서 최종적으로 Collectors.toUnmodifiableMap()함수를 사용하여 불변한 Map 자료형으로 변환을 해주는 것이다.

여기서 잠시 UnmodifiableMap.java를 들여다보자.

private static class UnmodifiableMap<K, V> implements Map<K, V>, Serializable {
		...생략
		
        // 예외처리 구간
        public V put(K key, V value) {
               throw new UnsupportedOperationException();
        }

        public V remove(Object key) {
               throw new UnsupportedOperationException();
        }

        public void putAll(Map<? extends K, ? extends V> m) {
               throw new UnsupportedOperationException();
        }

        public void clear() {
               throw new UnsupportedOperationException();
        }
}

코드에서 보는 것처럼 remove, pustAll, clear 등의 메소드를 호출할 경우 예외를 발생시키도록 설계되어있다. 즉, 변경이 발생하지 않도록 설계되어있는 것이다.

한가지 더 설명하자면 (OauthClient::oauthProvider) 이 부분은 OauthClient 객체들이 가지고 있는 oauthProvider()를 호출하라는 메소드 참조 표현식이며, 람다식에서 사용하는 표현식이다.

해당 객체의 생성자로 객체가 주입되는 시기는 하단의 reqeust()로 params를 받는 시점이다. 이렇게 주입된 객체는 OauthClient형으로 주입되어 토큰 발급 및 유저정보를 획득하는 메소드를 수행하게되는 것이다.

이제 이렇게 준비된 객체들을 이용해서 Service와 Controller에서 어떻게 호출하는지 알아보자.

🖥️ OAuthService.java

@Slf4j
@PropertySource("/WEB-INF/config/api.properties")
@RequiredArgsConstructor
@Component
public class OAuthService {
	private final MemberDAO memberDAO;
	private final RequestOauthInfoService requestOauthInfoService;
	private final JwtProvider jwtProvider;

	// 받아온 유저정보로 로그인 시도
	public String getMemberByOauthLogin(OauthParams oauthParams) {
		log.debug("------ Oauth 로그인 시도 ------");

		// 인증 파라미터 객체를 이용하여 해당 enum클래스에 해당하는 메소드 수행
		OauthMember oauthMember = requestOauthInfoService.request(oauthParams);
		log.debug("전달받은 유저정보:: " + oauthMember.getEmail());
		
		// 획득한 회원정보로 검증할 MemberDTO 생성
		Member accessMember = new Member();
		accessMember.setMemberId(oauthMember.getEmail());
		accessMember.setMemberName(oauthMember.getNickName());

		// 획득된 회원정보 DB 조회
		Member result = memberDAO.selectByOauthLogin(accessMember);

		// 반환할 JWT
		String accessJwt = null;

		if (result == null) {
			log.debug("------ 회원가입 필요한 회원 ------");
			// 회원가입이 되지 않은 회원이기 때문에 회원 DTO에 값을 전달하여 DB저장
			log.debug("회원가입 요청 :: " + accessMember.getMemberName());

			// kakaoMember에서 전달된 데이터를 가진 memberDTO DB 저장
			memberDAO.insert(accessMember);

			log.debug("회원가입 완료 :: " + accessMember.getMemberName());
		}
		// 이미 가입된 회원은 토큰발급
		log.debug("------ JWT 발급 ------");
		accessJwt = jwtProvider.createToken(accessMember.getMemberId());

		log.debug("------ JWT 발급완료 ------");
		return accessJwt;
	}
}

사실 OAuthService의 코드가 조금 신경쓰이지만 개인 프로젝트의 다른 기능들을 구현하고 리팩토링을 할 예정이다. 흐름은 FE로부터 넘겨받은 인가코드를 전달받는 시점에 요청 URL을 분석하고, 해당 Params를 전달한다. Params에 지정된 OauthProvider의 자료형에 해당하는 OauthClient를 생성하고, 토큰 발급 및 유저정보를 수집한다.
이후 수집된 유저정보를 DB에 접근할 Member 객체에 전달하여 DB와 대조 후 없으면 신규가입, 있으면 JWT를 반환한다.

여기서 JWT에 대한 코드가 궁금하다면 여기를 참고하자.

이렇게 Service까지 완료되었으면 Controller로 넘어가겠다.

🖥️ LoginController.java

...생략
	@PostMapping("/oauth/kakao")
	public ResponseEntity<String> handleKakaoLogin(@RequestBody KakaoParams kakaoParams){
		log.debug("넘겨받은 Kakao 인증키 :: " + kakaoParams.getAuthorizationCode());
		
		String accessToken = oauthService.getMemberByOauthLogin(kakaoParams);
		//응답 헤더 생성
		HttpHeaders headers = new HttpHeaders();
		headers.set("accessToken", accessToken);
		
		return ResponseEntity.ok().headers(headers).body("Response with header using ResponseEntity");
	}
	
	@PostMapping("/oauth/naver")
	public ResponseEntity<String> handleNaverLogin(@RequestBody NaverParams naverParams){
		log.debug("넘겨받은 naver 인증키 :: " + naverParams.getAuthorizationCode());
		
		String accessToken = oauthService.getMemberByOauthLogin(naverParams);
		//응답 헤더 생성
		HttpHeaders headers = new HttpHeaders();
		headers.set("accessToken", accessToken);
		
		return ResponseEntity.ok().headers(headers).body("Response with header using ResponseEntity");
	}

위처럼 각 요청에 맞는 Params만 전달해주면 된다. 그리고 응답받은 accessToken을 응답 헤더에 적재하여 FE로 보내주면 되는 것이다.

Spring Legacy의 특징인건지 아니면 내가 못하는 건지 모르겠지만 정말 엄청나게 많은 오류를 겪으며 완성시켰다. 이제 Google 로그인을 붙이고 싶다면 Client, Member, Token, Params 4가지 클래스만 특성에 맞게 준비하면 간편하게 붙일 수 있다.


마무리

오류에 대한 내용을 포함하고 싶지만 너무 길어지기에 다음 게시글에서 해당 기능을 구현하면서 겪었던 오류들에 대한 내용을 짧게나마 공유하려고 한다.

코드의 재사용과 JWT 등을 고려하지 않았다면 어쩌면 금방 끝냈을 것이다. 하지만 그냥 한번 기능만 보고 넘어가는 코드를 작성하고 싶지 않았기에 멍청할 순 있어도 3일 동안 머리를 싸매고 여러가지 시도를 했던 것들이 다행으로 생각된다.

구글링을 하면서 따라하는 것이 결코 나쁘지 않다는 것을 느끼게 된 계기였다. 구글링으로 찾은 내용을 내 프로젝트와 개발환경에 맞게 가공하는 것도 현 시점의 나에게는 많은 도움이되고, 왜 그렇게 했는지 고민하고 찾아가다보면 다른 방법이 생각나는 경우도 있어서 더욱 그런 것 같다.

그럼 이만.👊🏽
(에러와 함께 돌아올 예정.)

profile
서핑하는 개발자🏄🏽

0개의 댓글