[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
에서 어플리케이션 등록 후 서비스별로 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에 담긴 사용자 정보들을 이용하여 가입 여부를 확인하고 로그인, 회원가입 등을 해준다.
♣ 참고 및 인용
'웹 개발 한걸음' 카테고리의 다른 글
[Git 입문] 2. Git의 주요 명령어 간단히 써보기 (feat.Sourcetree) (0) | 2021.10.19 |
---|---|
[SQL] Left Join과 Inner Join 간단 정리 (0) | 2021.08.12 |
[OAuth2.0] 소셜 로그인 구현하기 -2- 카카오 어플리케이션 등록 (0) | 2021.07.28 |
[OAuth2.0] 소셜 로그인 구현하기 -1- OAuth란? (0) | 2021.07.23 |
[Spring+Mybatis] 로그인 구현하기 (0) | 2021.06.23 |