목적
카카오 구글 했는데 네이버 안하면 섭섭함
그런데 네이버 Developer의 API 명세에는 JSP 등 만 있고, Spring Boot JPA 환경이 없음
* (필독) 이 글은 Spring Boot JPA환경에서 JWT를 이용한 인증방식의 Spring Security사용자를 위함입니다.
환경
1. JPA ORM을 이용한 자체 서비스가 이미 구현되어 있음
2. Spring Security로 Security filter Chain이 구현되어 있음
3. JWT를 이용한 로그인 인증방식으로 구현되어있음
4. (로그아웃 로직에 한해서 필요) Redis를 이용한 Token 캐시 서버가 연동되어 있음
* 네이버 로그아웃 구현 : https://radpro.tistory.com/678
할 것 요약
1. 네이버 Developer 설정
2. Redirect URI 제공 API 만들기
3. Callback API 만들기
네이버 Developer 설정
: API를 만들기에 앞서, 네이버 Developer에서 어플리케이션 등록을 해줘야 사용이 가능하다.
: API에 필요한 요소와 규칙들은 공식문서에 포함되어 있으니 한번씩 읽어보길 권장한다. (링크)
1. Naver Developers 접속 : https://developers.naver.com/main/
2. 네이버 로그인 -> 오픈 API 이용 신청
3. 환경 추가 -> PC웹
4. 서비스 URL (예시 : http://localhost:8080) -> 본인 서버 주소. www 없어야함
5. 네이버 로그인 Callback URL (예시 : http://localhost:8080/naver/callback) -> 본인이 만들 callback API URL
* 등록 완료시 화면 (API 설정 탭에서 로고 이미지 등록을 권장)
Redirect URI 제공 API 만들기
목적 : 유저가 네이버 로그인을 희망할 경우 시작할 수 있도록, 클라이언트에서 네이버 로그인 창을 띄워줄 수 있도록 URI를 전송해줄 API
1. yml 설정 : 필자의 경우
oauth:
naver:
clientId: 본인의_ClientID
clientSecret: 본인의_Secret
2. Controller 내 핸들러 메소드 구현
: 네이버 지정 URL + 본인앱의_clientId + 암호화용_state값 + 인코딩된_Redirect_URL 이 반환되어야 한다.
/**
* 프론트에 Redirect URI를 제공하기 위한 메소드
* 프론트에서 네이버 인증 센터로 요청을 주기위한 URI를 제공하며, 이를통해 인증코드를 받아 자체 서비스 callback API 호출시 전달
*
* @return redirect URI
* @throws UnsupportedEncodingException
*/
@GetMapping("/oauth")
public ResponseEntity<?> naverConnect() throws UnsupportedEncodingException {
String url = naverService.createNaverURL();
return new ResponseEntity<>(url, HttpStatus.OK); // 프론트 브라우저로 보내는 주소
}
3. Service계층 내 비즈니스 로직
public String createNaverURL () throws UnsupportedEncodingException {
StringBuffer url = new StringBuffer();
// 카카오 API 명세에 맞춰서 작성
String redirectURI = URLEncoder.encode("http://www.localhost:8080/naver/callback", "UTF-8"); // redirectURI 설정 부분
SecureRandom random = new SecureRandom();
String state = new BigInteger(130, random).toString();
url.append("https://nid.naver.com/oauth2.0/authorize?response_type=code");
url.append("&client_id=" + getNaverClientId());
url.append("&state=" + state);
url.append("&redirect_uri=" + redirectURI);
/* 로그인 중 선택 권한 허용 URL로 redirect 문제 해결하기
로그인 시도시, "현재 UYouBooDan은 개발 중 상태입니다. 개발 중 상태에서는 등록된 아이디만 로그인할 수 있습니다." 화면으로 가버림.
아래와 같은 URL로 리다이렉트 되도록 유도하는 해결책 찾기
: https://nid.naver.com/oauth2.0/authorize?client_id=avgLtiDUfWMFfHpplTZh&redirect_uri=https://developers.naver.com/proxyapi/forum/auth/oAuth2&response_type=code&state=RZ760w
*/
return url.toString();
}
* 성공했을 경우, 브라우저에 표시될 화면 예시
* 아래는 네이버 로그인 연동 URL 생성 조건 공식문서 일부 발췌
Callback API 만들기
목적 : 유저가 네이버 인증센터에 로그인 요청을 보낼 경우, 네이버로부터 리다이렉트 되어 요청을 받아 콜백처리해줄 API
: 즉, 네이버 - 서버 인증 로직 수행 후 서버 - 클라이언트로 결과처리 해주기 위한 API
Callback API (핸들러 메소드) 에서 필요한 로직은 크게 3개로 구성된다.
1. Naver 인증 토큰을 발급받는 로직
2. 발급받은 인증 토큰으로 Naver로부터 유저의 Profile정보를 받아오는 로직
3. 서비스서버에서 해당 유저의 정보를 조회 후, 가입이 필요하면 가입을 시킨 뒤, 로그인 처리해주는 로직
요청 프로세스 :
1. 유저가 클라이언트를 통해 로그인 요청 (서비스서버로부터 받은 redirect_url과 state 전달)
2. Naver인증서버에서 서비스서버로 callback API 요청 (code와 state가 파라미터로 들어옴, state로 보안검증)
3. 서비스서버에서 Naver인증서버 요청 기준에 맞춰 Token발급 POST(또는 GET) 요청
4. 서비스서버에서 NAver인증서버 요청 기준에 맞춰 유저Profile POST(또는 GET) 요청 (Nave 토큰 전달)
5. Naver인증서버로부터 받은 유저의 Profile 정보를 바탕으로 서비스서버 회원가입 밎 로그인
6. 유저에게 토큰 발급 및 로그인 성공
1. 클라이언트 요청에 따라 Naver로부터 요청받는 로직
[클라이언트 -> Naver인증서버 -> 서비스서버 Callback 메소드]
: 유저가 클라이언트를 통해 Naver에 로그인 시도시, Naver가 서비스서버 Callback API에 제공하는 결과
: 상단의 Redirect_URL로 브라우저에서 로그인 시도를 하면 요청이 온다.
파라미터 :
1. code : Naver에서 발급
2. state : 서비스서버에서 발급해 클라를 통해 Naver에 전달했던 암호
성공시 : code, state, 200
실패시 : error
* 아래는 네이버 로그인 연동 결과 Callback 정보 공식문서 일부 발췌
2. 컨트롤러 내 Naver 인증 토큰을 발급 핸들러 메소드
[서비스서버 -> Naver서버 -> 서비스서버]
: Callback 메소드(API) 안에서 Naver로부터 Token을 받기위한 요청
: Token발급을 위해 Naver로 REST API 호출하는 로직과 응답으로 받은 Json정보를 담을 객체 클래스(VO)가 필요하다
* Controller 내 메소드 코드 스니펫
/**
* 실제 로그인 로직을 수행할 메소드
*
* 비즈니스 로직 성공 : @return "Success Login: User"
* 비즈니스 로직 실패 : @return "Fail Login: User"
*/
@GetMapping("/callback")
public String naverLogin(@RequestParam("code") String code, @RequestParam("state") String state, HttpServletResponse response) throws JsonProcessingException {
naverService.loginNaver(code, state, response);
/* Header가 아닌 Redis 서버에 잘 저장이 되었는지 확인하기 */
return response.getHeader("Authorization") == null ? "Fail Login: User" : "Success Login: User";
}
* Service 계층 내 비즈니스 로직
public void loginNaver (String code, String state, HttpServletResponse response) throws JsonProcessingException {
// 네이버 로그인 Token 발급 API 요청을 위한 header/parameters 설정 부분
RestTemplate token_rt = new RestTemplate(); // REST API 요청용 Template
HttpHeaders naverTokenRequestHeadres = new HttpHeaders(); // Http 요청을 위한 헤더 생성
naverTokenRequestHeadres.add("Content-type", "application/x-www-form-urlencoded"); // application/json 했다가 grant_type missing 오류남 (출력포맷만 json이라는 거엿음)
// 파라미터들을 담아주기위한 맵 (파라미터용이기 때문에, 따로 앞에 ?나 &나 =같은 부호를 입력해주지 않아도 됨. 오히려 넣으면 인식못함)
// 네이버 가이드에서 요청하는 파라미터들 (Developers 참고)
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", getNaverClientId());
params.add("client_secret", getNaverClientSecret());
params.add("code", code);
params.add("state", state);
HttpEntity<MultiValueMap<String, String>> naverTokenRequest =
new HttpEntity<>(params, naverTokenRequestHeadres);
// 서비스 서버에서 네이버 인증 서버로 요청 전송(POST 또는 GET이라고 공식문서에 있음), 응답은 Json으로 제공됨
ResponseEntity<String> oauthTokenResponse = token_rt.exchange(
"https://nid.naver.com/oauth2.0/token",
HttpMethod.POST,
naverTokenRequest,
String.class
);
// body로 access_token, refresh_token, token_type:bearer, expires_in:3600 온 상태
System.out.println(oauthTokenResponse);
// oauthTokenResponse로 받은 토큰정보 객체화
ObjectMapper token_om = new ObjectMapper();
NaverTokenVo naverToken = null;
try {
naverToken = token_om.readValue(oauthTokenResponse.getBody(), NaverTokenVo.class);
} catch (JsonMappingException je) {
je.printStackTrace();
}
* NaverTokenVo 클래스 코드 스니펫
package TeamBigDipper.UYouBooDan.global.oauth2.naver;
import lombok.Data;
@Data
public class NaverTokenVo {
private String access_token;
private String refresh_token;
private String token_type;
private int expires_in;
}
* 아래는 네이버 토큰 발급 방법 및 조건 공식문서 일부 발췌
* Naver 인증 서버에 요청을 보낼 URL
* 응답 포맷은 Json이지만, 요청 포맷은 x-www-form-urlencoded;charset=UTF-8 인 것이 함정이다.(주의)

* Naver 인증 서버에 요청을 보낼 때 담을 파라미터들

* 요청문 샘플
https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=jyvqXeaVOVmV&client_secret=527300A0_COq1_XV33cf&code=EIc5bFrl4RibFls1&state=9kgsGTfH4j7IyAkg
* 아래 정보에 맞춰 VO를 작성

3. 발급받은 인증 토큰으로 Naver로부터 유저의 Profile정보를 받아오는 로직
[서비스서버 -> Naver서버 -> 서비스서버]
: 2번에서 발급받은 토큰정보(access_token)를 통해 유저의 profile 정보를 요청
: Profile요청을 위해 Naver로 REST API 호출하는 로직과 응답으로 받은 Json정보를 담을 객체 클래스(VO)가 필요하다
* Controller 내 메소드 코드 스니펫 및 Service 계층 내 비즈니스 로직
* 비즈니스 로직(Service)
public void loginNaver (String code, String state, HttpServletResponse response) throws JsonProcessingException {
...
// 토큰을 이용해 정보를 받아올 API 요청을 보낼 로직 작성하기
RestTemplate profile_rt = new RestTemplate();
HttpHeaders userDetailReqHeaders = new HttpHeaders();
userDetailReqHeaders.add("Authorization", "Bearer " + naverToken.getAccess_token());
userDetailReqHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=UTF-8");
HttpEntity<MultiValueMap<String, String>> naverProfileRequest = new HttpEntity<>(userDetailReqHeaders);
// 서비스서버 - 네이버 인증서버 : 유저 정보 받아오는 API 요청
ResponseEntity<String> userDetailResponse = profile_rt.exchange(
"https://openapi.naver.com/v1/nid/me",
HttpMethod.POST,
naverProfileRequest,
String.class
);
// 요청 응답 확인
System.out.println(userDetailResponse);
// 네이버로부터 받은 정보를 객체화
// *이때, 공식문서에는 응답 파라미터에 mobile 밖에없지만, 국제전화 표기로 된 mobile_e164도 같이 옴. 따라서 NaverProfileVo에 mobile_e164 필드도 있어야 정상적으로 객체가 생성됨
ObjectMapper profile_om = new ObjectMapper();
NaverProfileVo naverProfile = null;
try {
naverProfile = profile_om.readValue(userDetailResponse.getBody(), NaverProfileVo.class);
} catch (JsonMappingException je) {
je.printStackTrace();
}
// 받아온 정보로 서비스 로직에 적용하기
Member naverMember = memberService.createNaverMember(naverProfile, naverToken.getAccess_token());
// 시큐리티 영역
// Authentication 을 Security Context Holder 에 저장
Authentication authentication = new UsernamePasswordAuthenticationToken(naverMember.getEmail(), naverMember.getPassword());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 자체 JWT 생성 및 HttpServletResponse 의 Header 에 저장 (클라이언트 응답용)
String accessToken = jwtTokenizer.delegateAccessToken(naverMember);
String refreshToken = jwtTokenizer.delegateRefreshToken(naverMember);
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("RefreshToken", refreshToken);
// RefreshToken을 Redis에 넣어주는 과정
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
valueOperations.set("RTKey"+naverMember.getMemberId(), refreshToken);
System.out.println(accessToken);
}
* Naver 인증 서버에 요청을 보낼 URL
* 응답 포맷은 Json이지만, 요청 포맷은 x-www-form-urlencoded;charset=UTF-8 인 것이 함정이다.(주의)

* Naver 인증 서버에 요청을 보낼 때 담을 파라미터들

* 아래 정보에 맞춰 VO를 작성
* 두 가지 함정이 존재한다.
* 'response/' 라고 적혀있는 부분은 depth가 하나 더 들어간 Json 정보이므로, VO 클래스에서 이너클래스로 해결
* 여기엔 mobile_e164라는 필드가 없으나, 실제론 같이 온다. 해당 필드를 VO에 선언해두지 않으면 오류발생

* NaverProfileVo 클래스 코드 스니펫
: 클래스 객체 안에 1depth를 추가하여 json정보를 받기위한 방법을 연습해 보기 좋다(설명 링크)
package TeamBigDipper.UYouBooDan.global.oauth2.naver;
import lombok.Data;
@Data
public class NaverProfileVo {
private String resultcode;
private String message;
private response response; // Json 형태로 주어지는 정보들을 담아줄 객체필드 생성 필수
// Json 형태로 담길 데이터를 받을 클래스(실질적인 정보들이다)
@Data
public class response {
private String id;
private String nickname;
private String name;
private String email;
private String gender;
private String age;
private String birthday;
private String profile_image;
private String birthyear;
private String mobile;
private String mobile_e164;
}
}
4. 서비스서버에서 해당 유저의 정보를 조회 후, 가입이 필요하면 가입을 시킨 뒤, 로그인 처리해주는 로직
[서비스서버->클라이언트]
: 이 로직은 필자의 서버 내 Spring Security에 맞춘 로직이므로, 각자의 서버 스타일에 맞추길 권장합니다.
* 서비스 서버내 회원가입&로그인 후 서비스서버 Security ContextHolder에 유저정보 저장, 자체 토큰 발급 코드스니펫
@GetMapping("/callback")
public String naverLogin(@RequestParam("code") String code, @RequestParam("state") String state, HttpServletResponse response) throws JsonProcessingException {
...
// 받아온 정보로 서비스 로직에 적용하기
Member naverMember = memberService.createNaverMember(naverProfile, naverToken.getAccess_token());
// 시큐리티 영역
// Authentication 을 Security Context Holder 에 저장
Authentication authentication = new UsernamePasswordAuthenticationToken(naverMember.getEmail(), naverMember.getPassword());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 자체 JWT 생성 및 HttpServletResponse 의 Header 에 저장 (클라이언트 응답용)
String accessToken = jwtTokenizer.delegateAccessToken(naverMember);
String refreshToken = jwtTokenizer.delegateRefreshToken(naverMember);
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("RefreshToken", refreshToken);
return "Success Login: User"; // 클라이언트 바디로 해당 메세지가 전달된다.
}
}
요청 방법
여기까지 API를 완성했다면, 요청을 보내는건 쉽다.
1. 아래의 URL로 요청을 보낸다.
http://localhost:8080/naver/oauth
2. 응답으로 온 URL을 브라우저에서 열면 Naver로그인 창이 뜬다.
3. 로그인 요청시, 정상적으로 로그인이 완료되면 아래의 문구가 브라우저에 뜬다.
Success Login: User
'Java & Spring > Spring' 카테고리의 다른 글
[OAuth2.0] Naver OAuth.20 API 요청 시 발생하는 권한 페이지 문제(진행중) (0) | 2023.05.17 |
---|---|
[OAuth2.0] 네이버(Naver) 로그아웃 - SpringBoot JPA JWT 방식용 (0) | 2023.05.11 |
[Redis] (5) EC2에 Redis 적용하기 - Ubuntu 사용 (0) | 2023.02.15 |
[Logging] p6spy 커스터 마이징 (0) | 2023.02.15 |
[JPQL] JPQL | Native Query 작성하기 (0) | 2023.02.15 |