본문 바로가기

웹 개발 한걸음

[OAuth2.0] 소셜 로그인 구현하기 -4- 카카오, 네이버, 구글 로그인

[OAuth2.0] 소셜 로그인 구현하기 -1- OAuth란?

[OAuth2.0] 소셜 로그인 구현하기 -2- 카카오 어플리케이션 등록

[OAuth2.0] 소셜 로그인 구현하기 -3- 카카오 로그인

[OAuth2.0] 소셜 로그인 구현하기 -4- 카카오, 네이버, 구글 로그인

 

 

** 생각보다 정리가 오래 걸렸다 ㅜㅜ

 

** 이번엔 scribejava라는 라이브러리를 이용해 카카오 뿐만 아니라 네이버, 구글 로그인도 같이 만들어볼 것이다. 

동작 방식이나 필요한 파라미터들은 앞의 글들에서 다뤘으므로 구현 위주로 작성할 것이다. 

 


 

    1. 라이브러리 추가    

 

<!-- scribejava-apis -->
	<dependency>
	    <groupId>com.github.scribejava</groupId>
	    <artifactId>scribejava-apis</artifactId>
	    <version>2.8.1</version>
	</dependency>
	
	<!-- scribejava-core -->
	<dependency>
	    <groupId>com.github.scribejava</groupId>
	    <artifactId>scribejava-core</artifactId>
	    <version>2.8.1</version>
	</dependency>

pom.xml에 라이브러리를 추가. 


 

 

   2. 소셜 서비스별 내 어플리케이션 등록   

 

카카오 : https://developers.kakao.com/console/app

네이버 : https://developers.naver.com/apps/#/register

구글 : https://console.cloud.google.com/apis/dashboard

 

에서 어플리케이션 등록 후 서비스별로 client_id, client_secret, redirect_uri를 준비해둔다.

여기선 하나의 컨트롤러, 하나의 DAO에서 소셜별로 구분하여 구현할 것이기 때문에 redirect는 모두 같게 해주었다. 

앞에선 임시로 전역변수로 관리했으나 이제는 프로퍼티즈를 만들어 관리할 것이다.

 

<!-- 프로퍼티즈 사용 -->
<context:property-placeholder location="classpath:config/social.properties" />

스프링 설정 xml에 프로퍼티즈를 사용할 것이라고 추가해두고 해당 경로에 프로퍼티즈를 만든다.

 

프로퍼티즈를 사용할 때 주의해야할 점은 값의 뒤에 띄어쓰기가 있진 않은지 꼭 확인해봐야한다. 이것 때문에 에러 찾는다고 5시간 버렸었다ㅜㅜ 프로퍼티즈에 서비스별로 저장해둔다.

 


    3. 인증 코드 요청    

 

<input type="button" onclick="location.href='login/social?socialType=kakao'"style="background: #FEE500; color: #191919; border:none;" class="btn btn-primary form-control" value="Login with Kakao">
<input type="button" onclick="location.href='login/social?socialType=naver'" style="background: #03C75A; color: #FFFFFF; border:none;" class="btn btn-primary form-control" value="Login with Naver">
<input type="button" onclick="location.href='login/social?socialType=google'" style="background: #ffffff; color: red; border:none;" class="btn btn-primary form-control" value="Login with Google">
@Controller
@RequestMapping("/login/")
public class SocialLoginCotroller {

	@Autowired
	SocialLoginDAO sao;
	
	@Autowired
	UserDAO uao;
	
	private String authorizationUrl;
	
	//소셜 로그인 유도
	@RequestMapping("social")
	public String socialLogin(String socialType, HttpSession session) {
		
		authorizationUrl = sao.getServiceURL(socialType);
		System.out.println("authorizationUrl = " + authorizationUrl);
		session.setAttribute("socialType", socialType);
		
		return "redirect:"+authorizationUrl;
		
	}

	.....
    
}

모든 소셜 로그인 요청은 소셜 별로 socialType과 함께 이 컨트롤러로 온다. 

SocialLoginDAO의 getServiceURL 메서드의 매개변수로 socialType을 넣어주고 소셜 로그인 주소를 받아 리다이렉트 시킨다.

 

@Service
public class SocialLoginDAO {
	
	@Value("${kakao.client_id}")
	private String kakao_client_id;
	@Value("${kakao.client_secret}")
	private String kakao_client_secret;
	
	@Value("${naver.client_id}")
	private String naver_client_id;
	@Value("${naver.client_secret}")
	private String naver_client_secret;
	
	@Value("${google.client_id}")
	private String google_client_id;
	@Value("${google.client_secret}")
	private String google_client_secret;

	@Value("${redirect_url}")
	private String redirect_url;

	//소셜 타입에 맞게 로그인하는 URL을 조립하여 전달해주는 메서드
	public String getServiceURL(String socialType) {
		DefaultApi20 api = socialInstance(socialType);
		Map<String, String> map = socialProperties(socialType);
		OAuth20Service service = null;
		switch (socialType) {
			case "kakao":
				service = new ServiceBuilder()
						.responseType("code")
						.apiKey(map.get("client_id"))
						.callback(redirect_url)
						.apiSecret(map.get("client_secret"))
						.build(api);
				break;
			case "google":
				service = new ServiceBuilder()
						.apiKey(map.get("client_id"))
						.apiSecret(map.get("client_secret"))
						.callback(redirect_url)
						.responseType("code")
						.scope("https://www.googleapis.com/auth/userinfo.profile")
						.build(api);
				break;		
			case "naver":
				service = new ServiceBuilder()
					.apiKey(map.get("client_id"))
					.apiSecret(map.get("client_secret"))
					.callback(redirect_url)
					.responseType("code")
					.build(api);
				break;
		}
		
		return service.getAuthorizationUrl();
	}
    
	//소셜별 API 인스턴스 조립 	
	public DefaultApi20 socialInstance(String socialType) {
		
		DefaultApi20 api = null;	
		switch (socialType) {
			case "kakao":
				api = KakaoLoginApi.instance();
				break;
			case "google":
				api = GoogleLoginApi.instance();
				break;		
			case "naver":
				api = NaverLoginApi.instance();
				break;
		}
		return api;
	}

	//소셜 타입에 맞게 oauth에 필요한 값을 map에 담아 전달해주는 메서드
	public Map<String, String> socialProperties(String socialType){

		Map<String, String> map = new HashMap<String, String>();

		switch (socialType) {
			case "kakao":
				map.put("client_id", kakao_client_id);
				map.put("client_secret", kakao_client_secret);
				map.put("reqURL", getProfileURL_kakao);
				break;
			case "google":
				map.put("client_id", google_client_id);
				map.put("client_secret", google_client_secret);
				map.put("reqURL", getProfileURL_google);
				break;		
			case "naver":
				map.put("client_id", naver_client_id);
				map.put("client_secret", naver_client_secret);
				map.put("reqURL", getProfileURL_naver);
				break;
		}
		return map;
	}

}
  • 이전 글에서는 HttpURLConnection으로 요청을 했다면 scribejava에선 OAuth20Service와 ServiceBuilder로 요청한다.
  • getServiceURL이 호출되면서 socialType이 전달과 함께 조립을 시작한다.
  • socialInstance 메서드에서는 socialType을 받고 소셜별로 ServiceBuilder에 필요한 API 구현체를 만들어 리턴해준다.
  • socialProperties 메서드에서는 socialType을 받고 소셜별로 client_id, client_secret, 소셜 서비스별 코드 요청 url을 맵에 담아 리턴해준다. 
  • getServiceURL에서는 받은 API구현체와 Map을 가지고 소셜 서비스별 로그인 URI를 조립하여 리턴되고 컨트롤러에서 리다이렉트 시킨다.

구글 로그인
카카오 로그인
네이버 로그인


    4. 토큰 요청 & 정보 요청    

 

@Controller
@RequestMapping("/login/")
public class SocialLoginCotroller {

	@Autowired
	SocialLoginDAO sao;
	
	@Autowired
	UserDAO uao;
	
	//소셜 로그인 후 code와 함께 여기로 리다이렉트됨.
	@RequestMapping("social/LoginAndJoin")
	public ModelAndView LoginAndJoin(@RequestParam("code")String code, HttpSession session) throws IOException {
		
		ModelAndView mv = new ModelAndView();
		//소셜 타입 꺼내기
		String socialType = (String) session.getAttribute("socialType");
		//엑세스 토큰 받기
		OAuth2AccessToken accessToken = sao.getkakaoAccessToken(code,socialType);
		//유저 프로필 받기
		Map<String, String> userInfo = sao.getUserProfile(accessToken, socialType);
		//DB에 접근해 유저 정보 조회
		String userCode = socialType+"&&"+userInfo.get("id")+"&&"+userInfo.get("email");
		UserVO userVO = uao.findSocialCode(userCode);
		
		if(userVO == null) { 
        	//등록된 유저가 아닐 때 -> 가입 처리
			
		}else {	
        	//등록된 유저일 때 -> 로그인 처리
			
		}
			
		return mv;	
	}


}

 

  • 사용자가 정상적으로 로그인하면 소셜 서비스 서버에서 설정해둔 redirect_uri로 인가 코드와 함께 redirect 될 것이다. 
  • 이 LoginAndJoin 컨트롤러 메서드가 redirect_uri에 매핑된다.
  • 소셜 타입의 유지를 위해 아까 세션에 넣었던 소셜 타입을 꺼낸다. 
  • 받은 인가 코드로 액세스 토큰을 요청하고 (getAccessToken())
  • 받은 액세스 토큰으로 유저 정보를 요청한다. (getUserProfile())
  • 이 어플리케이션은 userCode(소셜타입+개인 소셜 식별 id+유저이메일)로 유저를 식별한다. 
  • userCode를 만들어 DB에 저장이 되어있는지 조회해보고 그 여부에 따라 가입과 로그인처리를 해준다. (생략)

 

@Service
public class SocialLoginDAO {

	@Value("${kakao.client_id}")
	private String kakao_client_id;
	@Value("${kakao.client_secret}")
	private String kakao_client_secret;
	
	@Value("${naver.client_id}")
	private String naver_client_id;
	@Value("${naver.client_secret}")
	private String naver_client_secret;
	
	@Value("${google.client_id}")
	private String google_client_id;
	@Value("${google.client_secret}")
	private String google_client_secret;

	@Value("${redirect_url}")
	private String redirect_url;
	
	private String getProfileURL_google = "https://www.googleapis.com/oauth2/v3/userinfo";
	private String getProfileURL_naver = "https://openapi.naver.com/v1/nid/me";
	private String getProfileURL_kakao = "https://kapi.kakao.com/v2/user/me";
	
	

	// code를 이용해 엑세스토큰을 받는 메서드
	public OAuth2AccessToken getkakaoAccessToken(String code, String socialType) throws IOException {
		System.out.println(code);
		DefaultApi20 api = socialInstance(socialType);
		Map<String, String> map = socialProperties(socialType);
		
		OAuth20Service oauthService = new ServiceBuilder()
			.apiKey(map.get("client_id"))
			.apiSecret(map.get("client_secret"))
			.callback(redirect_url)
			.build(api);
		
		OAuth2AccessToken accessToken =oauthService.getAccessToken(code);
		System.out.println(accessToken);

		return accessToken;
	}
	
    // 받은 토큰으로 유저 정보를 가져오는 메서드
	public Map<String, String> getUserProfile(OAuth2AccessToken access_Token, String socialType) throws IOException {
		DefaultApi20 api = socialInstance(socialType);
		Map<String, String> map =  socialProperties(socialType);
		OAuth20Service oauthService = null;
		
		oauthService = new ServiceBuilder() 
				.apiKey(map.get("client_id"))
				.apiSecret(map.get("client_secret")) 
				.callback(redirect_url)
				.build(api); 
	
		String reqURL = map.get("reqURL");
		System.out.println(reqURL);
		OAuthRequest request = new OAuthRequest(Verb.GET,reqURL,oauthService);
		oauthService.signRequest(access_Token, request); 
		//request.addHeader("Authorization", access_Token.getAccessToken());
		 
		String body = request.send().getBody(); 
		
		Map<String,String> userInfo = getJsonElement(socialType,body);
		
		return userInfo;
	}

	//받은 유저 정보로부터 소셜 타입별로 정보 추출
	private Map<String, String> getJsonElement(String socialType, String body) {
		Map<String, String> userInfo = new HashMap<String, String>();
		JsonElement element = JsonParser.parseString(body);
		String id = null;
		String nickname = null;
		String email = null;
		
		if(socialType.equals("kakao")) {
			
			JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
	        JsonObject kakao_account = element.getAsJsonObject().get("kakao_account").getAsJsonObject();
	        
	        id = element.getAsJsonObject().get("id").getAsString();
	        nickname = properties.getAsJsonObject().get("nickname").getAsString();
	        email = kakao_account.getAsJsonObject().get("email").getAsString();
	        
		}else if(socialType.equals("naver")) {
			JsonObject ob = element.getAsJsonObject().get("response").getAsJsonObject();
		
			nickname = ob.getAsJsonObject().get("nickname").getAsString();
			email = ob.getAsJsonObject().get("email").getAsString();
			id = ob.getAsJsonObject().get("id").getAsString();
		
		}else if(socialType.equals("google")) {

			nickname = element.getAsJsonObject().get("name").getAsString();
			id = element.getAsJsonObject().get("sub").getAsString();
			email = element.getAsJsonObject().get("email").getAsString();
		}

		userInfo.put("id", id);	//필수
		userInfo.put("email", email);	//필수
		userInfo.put("nickname", nickname);	//필수

		return userInfo;
	}
	
    
	//소셜별 API 인스턴스 조립 	
	public DefaultApi20 socialInstance(String socialType) {
		
		DefaultApi20 api = null;	
		switch (socialType) {
		case "kakao":
			api = KakaoLoginApi.instance();
			break;
		case "google":
			api = GoogleLoginApi.instance();
			break;		
		case "naver":
			api = NaverLoginApi.instance();
			break;
		}
		return api;
	}
	
	
	
	//소셜 타입에 맞게 oauth에 필요한 값을 map에 담아 전달해주는 메서드
	public Map<String, String> socialProperties(String socialType){
		
		Map<String, String> map = new HashMap<String, String>();
		
		switch (socialType) {
		case "kakao":
			map.put("client_id", kakao_client_id);
			map.put("client_secret", kakao_client_secret);
			map.put("reqURL", getProfileURL_kakao);
			break;
		case "google":
			map.put("client_id", google_client_id);
			map.put("client_secret", google_client_secret);
			map.put("reqURL", getProfileURL_google);
			break;		
		case "naver":
			map.put("client_id", naver_client_id);
			map.put("client_secret", naver_client_secret);
			map.put("reqURL", getProfileURL_naver);
			break;
		}
		return map;
	}

	
	
}

 

  • getAccessToken메서드에서는 컨트롤러로부터 받은 인가코드와 소셜타입으로 액세스토큰을 요청한다. 
  • 요청에 필요한 3요소와 API구현체는 아까 인가코드 요청할 때 썼던 socialProperties와 socialInstance를 재사용하여 쉽게 요청하고 scribjava 라이브러리 메서드인 getAccessToken으로 쉽게 액세스 토큰을 얻을 수 있다. 

 

  • 컨트롤러에서는 리턴된 AccessToken과 socialType을 가지고 다시 getUserProfile 메서드를 호출한다.
  • socialType으로 다시 socialProperties와 socialInstance로 필요한 리소스들을 준비해두고 OAuthRequest로 소셜 서비스의 서버에 요청을 보낸다. (요청URL은 socialProperties를 통해 Map에 저장되어있다.)
  • 요청을 보내고 String타입의 body를 받는다. 이 body에는 처음 어플리케이션에 등록할 때 받기로 약속한 동의 항목에 대한 사용자의 정보가 담겨져 온다. 
  • 소셜별로 무엇이 올지 이미 알고 있기 때문에 getJsonElement(메서드 이름을 너무 대충 지은듯) 메서드에서 소셜별로 gson을 이용해 json으로 파싱하여 정보를 뽑고 저장하여 Map에 담아 리턴한다.
  • 컨트롤러에서 최종적으로 이 Map에 담긴 사용자 정보들을 이용하여 가입 여부를 확인하고 로그인, 회원가입 등을 해준다. 

 


 

 

 

 

♣ 참고 및 인용