[Dining-together] oauth 소셜 로그인(카카오, 네이버)

Jifrozen·2021년 6월 6일
0

Dining-together

목록 보기
9/25

oauth

OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다

카카오와 네이버 로그인 api를 사용해보도록 하겠다.

카카오

https://developers.kakao.com/
카카오 개발자에 들어가 앱을 등록한다.

플랫폼도 등록한다.

redirectURI 도 등록해준다!!

application.yml

  social:
    kakao:
      client_id: 
      redirect: /member/social/login/kakao
      url:
        login: https://kauth.kakao.com/oauth/authorize
        token: https://kauth.kakao.com/oauth/token
        profile: https://kapi.kakao.com/v2/user/me
  url:
    base: http://localhost:8000

yml파일에 등록해준다.
이제 뭘해야하지...?
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
공식문서를 보자. (한국말 최고...)

위 과정 비슷하게 네이버도 진행하삼요...

로그인 api를 사용하는 흐름을 알아보자.

  1. 인가 코드 받기
    프런트에서 받아서 넘겨줌
  2. 토큰 받기
    프런트측에서 인가 코드를 받은 뒤, 백에서 토큰을 카카오/네이버에게 요청합니다. 인가 코드 받기만으로는 카카오 로그인이 완료되지 않으며, 토큰 받기까지 마쳐야 카카오/네이버 로그인을 정상적으로 완료할 수 있습니다.
  3. 카카오네이버 토큰을 이용해 내 애플리케이션에서 토큰을 발급합니다.
  4. 토큰을 프런트에 전달합니다.

1. 인가코드 받기

짱 쉽다 공식문서 그대로 따라하면 된다.
https://kauth.kakao.com/oauth/authorize?client_id=받은 REST API 키&redirect_uri=설정한 uri&response_type=code

동의화면이 나오고 후 오류 화면이 나온다! 괜찮다 오류가 아니니깐 주소를 보면 code값이 무엇인지 보인다. 이게 인가코드이다.

2. 카카오 토큰 받기

https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&code={AUTHORIZATION_CODE}

이전에 받은 인가코드를 넣어준다.
그럼 토큰을 발행해준다
{
"token_type":"bearer",
"access_token":"{ACCESS_TOKEN}",
"expires_in":43199,
"refresh_token":"{REFRESH_TOKEN}",
"refresh_token_expires_in":25184000,
"scope":"account_email profile"
}
우리는 여기서 access_token을 이용해 로그인/회원가입을 할것이다.

Controller

	@ApiOperation(value = "소셜 로그인", notes = " 소셜 회원 로그인을 한다.")
	@PostMapping(value = "/signin/{provider}")
	public SingleResult<String> signinByProvider(
		@ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider,
		@ApiParam(value = "소셜 인가 코드", required = true) @RequestParam String code) {

		User signedUser = null;

		switch (provider) {
			case "naver":
				RetNaverAuth retNaverAuth=naverService.getNaverTokenInfo(code);
				signedUser = userService.signupByNaver(retNaverAuth.getAccess_token(), provider);
				break;
			case "kakao":
				RetKakaoAuth retKakaoAuth = kakaoService.getKakaoTokenInfo(code);
				signedUser = userService.signupByKakao(retKakaoAuth.getAccess_token(), provider);
				break;
		}

		return responseService.getSingleResult(
			jwtTokenProvider.createToken(String.valueOf(signedUser.getEmail()), signedUser.getRoles()));

	}

kakaoService

package kr.or.dining_together.member.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.google.gson.Gson;

import kr.or.dining_together.member.advice.exception.ComunicationException;
import kr.or.dining_together.member.vo.KakaoProfile;
import kr.or.dining_together.member.vo.RetKakaoAuth;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class KakaoService {

	private final RestTemplate restTemplate;
	private final Environment env;
	private final Gson gson;

	@Value("${spring.url.base}")
	private String baseUrl;

	@Value("${spring.social.kakao.client_id}")
	private String kakaoClientId;

	@Value("${spring.social.kakao.redirect}")
	private String kakaoRedirect;

	/**
	 * 카카오 플랫폼에서 사용자 정보를 요청한다.
	 **/
	public KakaoProfile getKakaoProfile(String accessToken) {
		// Set header : Content-type: application/x-www-form-urlencoded
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		headers.set("Authorization", "Bearer " + accessToken);

		// Set http entity
		HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(null, headers);
		try {
			// Request profile
			ResponseEntity<String> response = restTemplate.postForEntity(
				env.getProperty("spring.social.kakao.url.profile"), request, String.class);
			if (response.getStatusCode() == HttpStatus.OK)
				return gson.fromJson(response.getBody(), KakaoProfile.class);
		} catch (Exception e) {
			throw new ComunicationException();
		}
		throw new ComunicationException();
	}

	/**
	 * 카카오 플랫폼에서 사용자 토큰을 발급받는다.
	 **/
	public RetKakaoAuth getKakaoTokenInfo(String code) {
		// Set header : Content-type: application/x-www-form-urlencoded
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		// Set parameter
		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
		params.add("grant_type", "authorization_code");
		params.add("client_id", kakaoClientId);
		params.add("redirect_uri", "http://localhost8000/member/social/login/kakao");
		params.add("code", code);
		System.out.print(params.values().toString());
		// Set http entity
		HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
		ResponseEntity<String> response = restTemplate.postForEntity("https://kauth.kakao.com/oauth/token",
			request, String.class);
		// return response;
		System.out.println(response.getStatusCode());
		if (response.getStatusCode() == HttpStatus.OK) {
			System.out.println(response.getBody());
			return gson.fromJson(response.getBody(), RetKakaoAuth.class);
		}
		return null;
	}

}
package kr.or.dining_together.member.service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.google.gson.Gson;

import kr.or.dining_together.member.vo.NaverProfile;
import kr.or.dining_together.member.vo.RetNaverAuth;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class NaverService {

	private final RestTemplate restTemplate;
	private final Environment env;
	private final Gson gson;

	@Value("${spring.url.base}")
	private String baseUrl;

	@Value("${spring.social.naver.client_id}")
	private String naverClientId;

	@Value("${spring.social.naver.client_secret}")
	private String naverClientSecret;

	@Value("${spring.social.naver.redirect}")
	private String naverRedirect;

	@Value("${spring.social.naver.url.profile}")
	private String naverProfile;

	private static String get(String apiUrl, Map<String, String> requestHeaders) {
		HttpURLConnection con = connect(apiUrl);
		try {
			con.setRequestMethod("GET");
			for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
				con.setRequestProperty(header.getKey(), header.getValue());
			}

			int responseCode = con.getResponseCode();
			if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
				return readBody(con.getInputStream());
			} else { // 에러 발생
				return readBody(con.getErrorStream());
			}
		} catch (IOException e) {
			throw new RuntimeException("API 요청과 응답 실패", e);
		} finally {
			con.disconnect();
		}
	}

	private static HttpURLConnection connect(String apiUrl) {
		try {
			URL url = new URL(apiUrl);
			return (HttpURLConnection)url.openConnection();
		} catch (MalformedURLException e) {
			throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
		} catch (IOException e) {
			throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
		}
	}

	private static String readBody(InputStream body) {
		InputStreamReader streamReader = new InputStreamReader(body);

		try (BufferedReader lineReader = new BufferedReader(streamReader)) {
			StringBuilder responseBody = new StringBuilder();

			String line;
			while ((line = lineReader.readLine()) != null) {
				responseBody.append(line);
			}

			return responseBody.toString();
		} catch (IOException e) {
			throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
		}
	}

	public NaverProfile getNaverProfile(String accessToken) {
		String header = "Bearer " + accessToken; // Bearer 다음에 공백 추가
		System.out.println(header);
		Map<String, String> requestHeaders = new HashMap<>();
		requestHeaders.put("Authorization", header);
		String responseBody = get(naverProfile, requestHeaders);
		System.out.println(responseBody);
		return gson.fromJson(responseBody, NaverProfile.class);
	}

	public RetNaverAuth getNaverTokenInfo(String code) {
		// Set header : Content-type: application/x-www-form-urlencoded
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		// Set parameter
		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
		params.add("grant_type", "authorization_code");
		params.add("client_id", naverClientId);
		params.add("client_secret", naverClientSecret);
		params.add("redirect_uri", "http://localhost:8000/member/social/login/naver");
		params.add("code", code);
		params.add("state", "dining");
		// Set http entity
		HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
		ResponseEntity<String> response = restTemplate.postForEntity(env.getProperty("spring.social.naver.url.token"),
			request, String.class);
		System.out.println(response.getStatusCode());
		if (response.getStatusCode() == HttpStatus.OK) {
			System.out.println(response.getBody());
			return gson.fromJson(response.getBody(), RetNaverAuth.class);
		}
		return null;
	}
}

3. 카카오 토큰 이용해서 애플리케이션 로그인 회원가입

우선 엑세스토큰을 이용해 사용자 존재 유무를 파악한다.
없으면 -> 회원가입
있으면 -> 로그인을 진행한다.

UserService - signinByProvider


	public User signupByKakao(String accessToken, String provider) {
		KakaoProfile profile = kakaoService.getKakaoProfile(accessToken);
		KakaoProfile.Kakao_account kakaoAccount = profile.getKakao_account();
		Optional<User> user = userRepository.findByEmailAndProvider(String.valueOf(kakaoAccount.getEmail()),
			provider);
		if (user.isPresent()) {
			return user.get();
		} else {
			Customer kakaoUser = Customer.builder()
				.email(String.valueOf(kakaoAccount.getEmail()))
				.name(kakaoAccount.getEmail())
				.gender(kakaoAccount.getGender())
				.provider(provider)
				.build();


			return (User)userRepository.save(kakaoUser);
		}
	}

	public User signupByNaver(String accessToken, String provider) {
		System.out.print(accessToken);
		NaverProfile naverProfile = naverService.getNaverProfile(accessToken);
		NaverProfile.Response naverAccount = naverProfile.getResponse();
		System.out.print(naverAccount.getEmail());
		Optional<User> user = userRepository.findByEmailAndProvider(naverAccount.getEmail(), provider);
		if (user.isPresent()) {
			return user.get();
		} else {
			Customer naverUser = Customer.builder()
				.email(naverAccount.getEmail())
				.name(naverAccount.getEmail())
				.provider(provider)
				.build();

			userRepository.save(naverUser);

			return naverUser;
		}
	}

참고문서

https://daddyprogrammer.org/post/1012/springboot2-rest-api-social-login-kakao/

0개의 댓글