https://developers.naver.com/main/
위 사이트에서 기본적인 서비스 URL과 Callback URL 같은 설정은 맞췄다고 가정하겠습니다.
네이버 로그인 API를 통해서 회원가입까지 구현하기 위해서 기본적인 로직은 크게 5단계로 이루어져 있다고 봅니다.
1. 로그인 버튼
API를 사용하려면 우선 로그인 버튼에 내 어플리케이션의 이용 동의를 받는 링크를 설정하는 것입니다.
1. naverlogin.jsp
<%@ page="" import="java.net.URLEncoder" %="">
<%@ page="" import="java.security.SecureRandom" %="">
<%@ page="" import="java.math.BigInteger" %="">
<%@ page="" contentType="text/html;charset=UTF-8" language="java" %="">
<html>
<head>
<title>네이버로그인</title>
</head>
<body>
<% String="" clientId="YOUR_CLIENT_ID" ;="" 애플리케이션="" 클라이언트="" 아이디값";=""
redirectURI="URLEncoder.encode("YOUR_CALLBACK_URL"," "UTF-8");=""
SecureRandom="" random="new" SecureRandom();=""
state="new" BigInteger(130,="" random).toString();=""
apiURL="https://nid.naver.com/oauth2.0/authorize?response_type=code"
+="&client_id=" clientId;="" redirectURI;=""
state;="" session.setAttribute("state",="" state);="" %="">
<a href="<%=apiURL%>"><img height="50" src="http://static.nid.naver.com/oauth/small_g_in.PNG"/></a>
</body>
</html>
naver developers의 튜토리얼에는 이런식으로 쓰여있습니다.
하지만 저는 클라이언트 아이디나 콜백 주소 그리고 state는 페이지에서 보여주기 싫어서
로그인 버튼이 보여주는 페이지로 넘어갈 때 컨트롤러에서 객체로 담아 페이지로 값을 넘겨주었습니다.
그러기 위해선 api 관련한 정보를 담을 클래스가 필요해서 그걸 먼저 만드시는 게 좋습니다.
public class SnsVO {
// naver developers 내 어플리케이션에서 확인
private String naver_client_id = "본인의 클라이언트 아이디";
// naver developers 내 어플리케이션에서 확인
private String naver_client_secret = "본인의 클라이언트 비밀번호";
// 여러개 있을 수 있으니 비워두고 필요할 상황에 set해서 사용가능
private String naver_redirect_uri = "설정한 리다이렉트 주소";
private String grant_type;
private String code;
private String state;
private String refresh_token;
private String access_token;
private String token_type;
private String expires_in;
private String apiURL;
}
getter/setter를 만드시고 그 후에는 네이버 로그인 관련 메서드를 관리하는 클래스를 만들었습니다.
거기서 a 태그에 설정할 링크를 받아오는 메서드를 우선 만들어 보도록 하겠습니다.
//로그인 관련 vo 객체 profile 객체는 좀더 아래에 나올 예정
SnsVO vo = new SnsProfileVO();
public SnsVO loginApiURL() throws UnsupportedEncodingException {
vo.setNaver_redirect_uri(URLEncoder.encode(vo.getNaver_redirect_uri(), "UTF-8"));
SecureRandom random = new SecureRandom();
vo.setState(new BigInteger(130, random).toString());
String apiURL = "https://nid.naver.com/oauth2.0/authorize?response_type=code";
apiURL += "&client_id=" + vo.getNaver_client_id();
apiURL += "&redirect_uri=" + vo.getNaver_redirect_uri();
apiURL += "&state=" + vo.getState();
vo.setApiURL(apiURL);
return vo;
}
위 코드를 보시면 vo객체에 apiURL을 저장합니다.
네이버 측에서 리다이렉트 주소를 인코딩해서 보내달라고 요청했으니 잊지 마세요.
loginApiURL 메서드를 통해서 받은 vo 객체를 컨트롤러에서 페이지로 보내는 일만 남았습니다.
spring의 home에 버튼이 있다고 가정하겠습니다.
//컨트롤러
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Model model, HttpServletRequest request, HttpSession session) throws UnsupportedEncodingException {
// 네이버 apiurl을 받아온다.
SnsVO navervo = naverservice.loginApiURL();
session.setAttribute("state",navervo.getState());
model.addAttribute("navervo", navervo);
return "home";
}
//HOME.jsp
<a href="${navervo.apiURL}">
<img height="50" src="http://static.nid.naver.com/oauth/small_g_in.PNG"/>
</a>
튜토리얼과 다르게 뷰페이지에서 간단하게 버튼을 생성했습니다.
2. Callback
다음으로 로그인 버튼의 링크 주소와 callback주소에 별다른 이상이 없다면
리다이렉트 주소로 code값과 state값이 반환됩니다.
여기서는 리다이렉트 주소를 http://localhost:8090/controller/callback.do으로 가정하겠습니다.
우리는 위에서 sns vo를 만들어 뒀기 때문에 리다이렉트를 받는 해당 컨트롤러 매개변수에 snsvo를
넣어두면 값을 무리 없이 받을 수 있습니다.
@RequestMapping(value="/callback.do")
public String naverLogin(SnsVO snsvo) throws IOException {
System.out.println(snsvo.getCode());
return "common/naverCallback";
}
리턴 값은 페이지로 보낼지 다른 서비스 로직을 구현할지에 따라 다르지만
컨트롤러 매개변수에 snsvo만 설정해두면 값을 받을 수 있다는 것을 인지하는 게 중요합니다.
sysout으로 제대로 값을 받아왔는지 확인해도 좋습니다.
3. Access_Token 발급
이제 accseeToken 까지만 발급받으면 로그인 부분은 끝이라고 할 수 있습니다.
새로 페이지나 버튼, 컨트롤러를 만들어서 토큰을 받아올 수도 있으나
여기서는 리다이렉트 컨트롤러에 추가로 구현해보겠습니다.
그러기 위해서는 먼저 메서드를 만들어야 합니다.
네이버 로그인 개발 가이드를 보면 get/post 두 방식으로 요청을 할 수 있고 json으로 응답합니다.
public SnsVO getAccessToken(SnsVO snsVO) {
String apiURL;
apiURL = "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&";
apiURL += "client_id=" + snsVO.getNaver_client_id();
apiURL += "&client_secret=" + snsVO.getNaver_client_secret();
apiURL += "&redirect_uri=" + snsVO.getNaver_redirect_uri();
apiURL += "&code=" + snsVO.getCode();
apiURL += "&state=" + snsVO.getState();
try {
URL url = new URL(apiURL);
HttpURLConnection con = (HttpURLConnection)url.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader br;
if(responseCode==200) {
br = new BufferedReader(new InputStreamReader(con.getInputStream()));
} else {
br = new BufferedReader(new InputStreamReader(con.getErrorStream()));
}
String inputLine;
while ((inputLine = br.readLine()) != null) {
ObjectMapper mapper = new ObjectMapper();
snsVO = mapper.readValue(inputLine, SnsVO.class);
}
br.close();
} catch (Exception e) {
System.out.println(e);
}
return snsVO;
}
getAccessToken(SnsVO snsVO) 메서드의 snsVO는 리다이렉트 주소로 반환된 code값을 사용하기 위해 설정했습니다.
HttpURLConnection 클래스를 통해서 설정한 apiURL 주소에 get방식으로 요청을 보냅니다.
만약 api주소가 정확하여 응답 코드가 200이면 응답 데이터를 버퍼에 담습니다.
하지만 응답 데이터는 json 형식이라 우리가 사용하기 쉽게 맞춰줄 필요가 있습니다.
버퍼에 담긴 값을 ObjectMapper의 readValue 메서드를 이용하면 snsVO 객체에 해당하는 멤버 변수에
자동으로 맞춰 값을 담아줍니다.
혹시 받아온 json 데이터에서 객체에 없는 값을 무시하고 싶으시면 아래 코드 한 줄 추가하면 됩니다.
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
그러면 이제 컨트롤러의 코드는 이렇습니다.
@RequestMapping(value="/callback.do")
public String naverLogin(SnsVO snsvo) throws IOException {
System.out.println(snsvo.getCode());
snsvo = naverService.getAccessToken(snsvo);
return "common/naverCallback";
}
naverService는 해당 메서드가 있는 클래스입니다.
그리고 그 값을 받는 snsvo는 새로운 객체를 생성해서 받아도 상관없습니다.
4. Access_Token으로 유저 프로필 정보 받기
유저 프로필을 받는 이유는 여러 가지가 있을 수 있지만 저는 api를 통해 회원가입 기능도 구현하려고
유저 프로필 정보를 받았습니다.
또한 프로필 정보도 json 형식으로 응답하기 때문에 ObjectMapper로 객체에 저장하는 과정까지 해보겠습니다.
그 이후로 데이터 베이스와 비교해 회원인지 아닌지 확인하는 과정이나 추가 데이터를 받는 것까지는 하지 않겠습니다.
이 과정은 좀 길어서 세 개의 메서드로 나눴습니다.
그전에 프로필 정보를 저장할 vo 클래스를 만들어주세요.
public class SnsProfileVO extends SnsVO {
private String resultcode;
private String message;
@JsonProperty
private JsonNode response;
private String age;
private String mobile;
private String id;
private String name;
private String nickname;
private String email;
private String gender;
private String birthyear;
private String birthday;
private String age_range;
}
getter/setter를 생성해주세요.
response는 데이터 타입이 JsonNode인데 응답 데이터의 형식이 아래와 같아서 한 번에 객체에 저장하려고 그렇습니다.
{
"resultcode": "00",
"message": "success",
"response": {
"email": "openapi@naver.com",
"nickname": "OpenAPI",
"profile_image": "https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif",
"age": "40-49",
"gender": "F",
"id": "32742776",
"name": "오픈 API",
"birthday": "10-01"
}
}
만약 sns 로그인 API를 네이버 하나만 사용하던지 각각 따로 나눠 사용할 거면
데이터 타입을 JsonNode로 하는 게 아니라 내부 클래스를 이용하는 게 훨씬 편합니다.
다음으로는 프로필 정보를 받아오는 메서드입니다.
public SnsProfileVO getUserProfile(SnsVO snsVO) throws IOException {
SnsProfileVO snsProfile = new SnsProfileVO();
String token = snsVO.getAccess_token();
String header = "Bearer " + token;
String apiURL = "https://openapi.naver.com/v1/nid/me";
Map<String, String> requestHeaders = new HashMap<String, String>();
requestHeaders.put("Authorization", header);
// get은 만든 메서드
snsProfile = get(apiURL, requestHeaders);
return snsProfile;
}
private static SnsProfileVO get(String apiURL, Map<String,String> requestHeaders) throws IOException {
URL url = new URL(apiURL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
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) {
// readyBody는 아래에 만든 메서드
return readyBody(con.getInputStream());
}else {
return readyBody(con.getErrorStream());
}
}catch(IOException e){
throw new RuntimeException("API Error", e);
}finally {
con.disconnect();
}
}
private static SnsProfileVO readyBody(InputStream body) {
InputStreamReader streamReader = new InputStreamReader(body);
SnsProfileVO snsProfile = new SnsProfileVO();
try(BufferedReader lineReader = new BufferedReader(streamReader)){
String line;
while((line = lineReader.readLine()) != null) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
snsProfile = mapper.readValue(line, SnsProfileVO.class);
snsProfile = mapper.readValue(snsProfile.getResponse().toString(), SnsProfileVO.class);
}
return snsProfile;
}catch(IOException e) {
throw new RuntimeException("API bufferedReader error", e);
}
}
for(Map.Entry<String, String> header : requestHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
for문으로 헤더에 토큰 값을 저장합니다. 그리고 요청을 보내 응답을 받습니다.
snsProfile = mapper.readValue(line, SnsProfileVO.class);
snsProfile = mapper.readValue(snsProfile.getResponse().toString(), SnsProfileVO.class);
두 번의 readValue로 프로필 값을 객체에 저장합니다.
그런데 이렇게 할 경우 객체가 두 개 생성되는 문제를 발견했습니다.
프로필 객체의 response 변수에 담겼던 값만 사용한다면 문제가 없지만
accessToken이나 code를 사용하려면 다른 식으로 저장해야 합니다.
이 문제 때문에 로그인 API를 하나만 사용할 거라면 앞서 이야기한 내부 클래스를 이용하는 게 좋습니다.
여기까지 하면 리다이렉트 주소로 한 번에 프로필 정보까지 받을 수 있습니다.
@RequestMapping(value="/callback.do")
public String naverLogin(SnsVO snsvo) throws IOException {
System.out.println(snsvo.getCode());
snsvo = naverService.getAccessToken(snsvo);
SnsProfileVO snsProfile = naverService.getUserProfile(snsvo);
return "common/naverCallback";
}
이제 프로필 정보까지 받아왔으니 구현하려는 기능에 맞춰 Callback 주소를 설정하면 됩니다.
- 로그인
- 회원가입
- 토큰 갱신
- 네이버 이용동의 연결해제 등
원하는 기능에 맞춰 메서드를 사용해 구현할 수 있습니다.
이번에 팀 프로젝트를 하면서 구현해본 API입니다.
부족한 점이 있을 수 있으니 너그러이 봐주시고 고칠 점을 알려주시면 고맙습니다.
카카오나 구글도 비슷하기 때문에 기회가 된다면 다음에 작성해보겠습니다.
'Java' 카테고리의 다른 글
spring scheduler 이용해서 매시각 메서드 실행하기 (0) | 2022.08.02 |
---|---|
JSTL foreach 한 리스트에서 객체 두 개씩 뽑아 사용하기 (0) | 2022.07.20 |
spring DI, AOP (0) | 2022.06.14 |
Spring의 특징 POJO/ DI/ IoC (0) | 2022.06.13 |
토이 프로젝트 주식 커뮤니티 만들기(4) - 클래스 다이어그램 (0) | 2022.05.19 |