이번에 진행하는 팀프로젝트에서 소셜 로그인 기능을 구현해야 했다. 로그인 방식은 JWT 기반 로그인 방식으로, 프론트 측에서 인가코드를 가지고 백엔드 서버로 요청을 보내는 방식으로 구현하였다.
로직 흐름을 나타낸 그림 중 가장 깔끔하게 정리되어있어, 해당 그림을 참조하며 구현하였다.
간단하게 설명하면, 프론트 측에서는 직접 카카오 및 네이버에 인가코드를 요청하여 얻어온 뒤에, 해당 인가코드를 가지고 백엔드 측에 로그인 요청을 날리는 방식이다. 백엔드는 이후에 자체적으로 JWT 토큰을 생성해 프론트 측에 넘겨준다.
기본적인 애플리케이션 설정(카카오 디벨로퍼, 네이버 디벨로퍼) 내용은 생략했다.
//카카오 인가코드 생성
//네이버 인가코드 생성
각각 카카오와 네이버 인가코드 생성 코드이다. 해당 코드는 프론트 측에서 이루어져야 하며, 요청을 보내면 카카오, 네이버 서버측으로부터 URL을 통해 인가코드를 받는다. 해당 인가코드를 추후에 백엔드 서버측에 보내주어야 한다.
위와 같이 요청시에 URL에 코드 값이 담겨오는 것을 확인할 수 있다.
개인 프로젝트시에는 서버사이드 렌더링을 통해 구현했기 때문에, 고려하지 않았던 부분들이, 팀프로젝트시에 문제가되어 꽤나 고생했다.
OAuth2KaKaoController.java
@RestController
@RequiredArgsConstructor
public class OAuth2KakaoController {
private final KakaoService kakaoService;
@GetMapping("/oauth2/kakao")
public ResponseEntity<?> kakaoCallback(@RequestParam("code") String code) throws IOException {
KakaoTokenDto kakaoTokenDto = kakaoService.getKakaoToken(code);
TokenDto tokenDto = kakaoService.loginWithKakao(kakaoTokenDto);
TokenResponseDto tokenResponseDto = TokenResponseDto.builder()
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.roles(tokenDto.getGrantType())
.build();
return ResponseEntity.status(HttpStatus.OK).body(tokenResponseDto);
}
}
OAuth2NaverController.java
@RestController
@RequiredArgsConstructor
public class OAuth2NaverController {
private final NaverService naverService;
@GetMapping("/oauth2/naver")
public ResponseEntity<?> naverCallback(@RequestParam("code") String code, @RequestParam("state") String state) throws IOException {
NaverTokenDto naverTokenDto = naverService.getNaverToken(code, state);
TokenDto tokenDto = naverService.loginWithNaver(naverTokenDto);
TokenResponseDto tokenResponseDto = TokenResponseDto.builder()
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.roles(tokenDto.getGrantType())
.build();
return ResponseEntity.status(HttpStatus.OK).body(tokenResponseDto);
}
}
NaverService.java 중 일부
public NaverTokenDto getNaverToken(String code, String state) {
String access_Token = "";
String refresh_Token = "";
String reqURL = "https://nid.naver.com/oauth2.0/token";
String result = null;
try{
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//POST 요청을 위해 기본값이 false인 setDoOutput을 true로
conn.setRequestMethod("POST");
conn.setDoOutput(true);
//POST 요청에 필요로 요구하는 파라미터 스트림을 통해 전송
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
StringBuilder sb = new StringBuilder();
sb.append("grant_type=authorization_code");
sb.append("&client_id=" + client_id);
sb.append("&client_secret=" + client_secret);
System.out.println("code = " + code);
sb.append("&code=" + code);
sb.append("&state=" + state);
bw.write(sb.toString());
bw.flush();
//응답 코드가 200이면 성공
int responseCode = conn.getResponseCode();
log.info("responseCode={}", responseCode);
//요청을 통해 얻은 JSON타입의 Response 메세지 Read
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
result = "";
while ((line = br.readLine()) != null) {
result += line;
}
log.info("response body={}", result);
// Gson 라이브러리에 포함된 클래스로 JSON파싱
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(result);
access_Token = element.getAsJsonObject().get("access_token").getAsString();
refresh_Token = element.getAsJsonObject().get("refresh_token").getAsString();
log.info("네이버 access token={}", access_Token);
log.info("네이버 refresh token={}", refresh_Token);
br.close();
bw.close();
conn.disconnect();
}catch (Exception e){
e.printStackTrace();
}
NaverTokenDto naverTokenDto = NaverTokenDto.builder()
.accessToken(access_Token)
.refreshToken(refresh_Token)
.build();
return naverTokenDto;
}
NaverService.java 중 일부
public TokenDto loginWithNaver(NaverTokenDto naverTokenDto) throws IOException {
//회원 정보 요청 url
String reqURL = "https://openapi.naver.com/v1/nid/me";
//accessToken 통한 사용자 정보 조회
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Authorization", "Bearer " + naverTokenDto.getAccessToken()); //전송할 header 작성, access_token전송
//결과 코드가 200이면 성공
int responseCode = conn.getResponseCode();
log.info("responseCode={}", responseCode);
//요청을 통해 얻은 JSON타입의 Response 메세지 Read
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
String result = "";
while ((line = br.readLine()) != null) {
result += line;
}
log.info("response body={}", result);
//Gson 라이브러리로 JSON파싱
JsonElement element = JsonParser.parseString(result);
String email = element.getAsJsonObject().get("response").getAsJsonObject().get("email").getAsString();
//DB에 해당 이메일 없을 경우 회원 가입 로직 실행
if (!memberRepository.existsByEmail(email)) {
Member member = Member.builder()
.email(email)
.password("")
.roles(Collections.singletonList("ROLE_USER"))
.build();
memberRepository.save(member);
TokenDto tokenDto = jwtTokenProvider.createToken(email, member.getRoles());
tokenDto.setGrantType(member.getRoles().get(0));
jwtService.saveRefreshToken(tokenDto);
return tokenDto;
} else {
Optional<Member> member = memberRepository.findByEmail(email);
//DB에 해당 이메일 회원 정보 있을 경우 jwt token 생성해서 리턴
TokenDto tokenDto = jwtTokenProvider.createToken(email, member.get().getRoles());
tokenDto.setGrantType(member.get().getRoles().get(0));
jwtService.saveRefreshToken(tokenDto);
return tokenDto;
}
}
카카오 로그인 같은 경우에도 전체적인 흐름과 로직은 똑같이 구현하였다. 전체 소스코드는 아래 깃허브에서 확인할 수 있다.