- Spring Boot 프레임워크에서 OAuth 2.0 프로토콜을 사용하여 ‘인증 및 권한부여’를 쉽게 구현할 수 있게 해주는 프레임워크입니다. - OAuth 2.0 프로토콜을 사용하여 ‘사용자 인증’을 하며 다양한 애플리케이션(Kakao, naver, Google..)에서 안전하게 사용자 정보에 접근할 수 있도록 합니다.
[더 알아보기] 💡 OAuth 2
- 인터넷 사용자들이 특정 웹 사이트를 접근하고자 할 때 '접근하려는 웹 사이트에 비밀번호를 제공하지 않고' 서드파티 애플리케이션(구글, 카카오, 페이스북 등)의 연결을 통해 '인증 및 권한'을 부여받을 수 있는 프로토콜을 의미합니다.
💡 프로토콜
- 인터넷에서 컴퓨터와 컴퓨터 간에 데이터를 주고받을 때 사용되는 통신 규약을 의미합니다.
1. Spring Boot OAuth 2.0 Client 특징
특징
설명
간편한 설정
Spring Boot의 자동 구성 기능을 통해 OAuth2 클라이언트 및 리소스 서버를 쉽게 설정할 수 있습니다.
다양한 그랜트 타입 지원
인증 코드, 암시적, 리소스 소유자 비밀번호 자격 증명, 클라이언트 자격 증명 등 다양한 OAuth2 그랜트 타입을 지원합니다.
다양한 인증 공급자 지원
Google, Facebook, GitHub, Okta, OAuth2를 지원하는 많은 인증 공급자를 지원합니다.
소셜 로그인 통합
Google, Facebook, GitHub 등 다양한 소셜 미디어 플랫폼과의 OAuth2 인증을 쉽게 구현할 수 있습니다.
토큰 관리
액세스 토큰 및 리프레시 토큰의 발급, 저장, 갱신을 자동으로 처리합니다.
보안
Spring Security를 사용하여 인증과 권한 부여를 처리합니다.
2. Spring Boot OAuth 2.0과 Spring Boot OAuth 2.0 Client 차이
💡 Spring Boot OAuth 2.0과 Spring Boot OAuth 2.0 Client 차이
💡 Spring Boot OAuth 2.0 : 제공자(Provider)
- OAuth 2.0 서비스를 제공하기 위한 목적으로 제공자(Provider)가 되어서, 다른 애플리케이션에서 구성한 해당 서비스를 사용하기 위한 목적으로 사용이 됩니다. - 그렇기에 인증 서버(Authorization Server)와 리소스 서버(Resource Server)를 구축해야 합니다. - 예를 들어서, 티스토리에 로그인을 하기 위해서는 ‘카카오 로그인’을 수행해야 합니다. 이 중 카카오 로그인 부분을 구현하는 개념과 같습니다. 이를 통해 티스토리에서 카카오 로그인을 통해 로그인이 가능합니다.
💡 Spring Boot OAuth 2.0 Client
- OAuth 2.0 서비스를 사용하기 위한 목적으로 OAuth 2.0 제공자(kakao, naver, google)로부터 ‘인증’을 수행하여 허용되면, 인가에 따르는 API 서버의 리소스에 대한 접근을 허용받아서 사용하기 위한 목적으로 사용이 됩니다.
- OAuth 2.0 제공자(Provider)의 인증 서버와 리소스 서버를 이용하기에 별도로 구축하지 않습니다. - 예를 들어서, API 서버를 구축할 때, 내부 로그인 기능을 구성하고 ‘외부 로그인’으로 OAuth 2.0 제공자(kakao, naver, google)로부터 인증을 수행하고 해당 서비스의 리소스를 사용할 수 있도록 제공해 주는 경우를 의미합니다.
💡 [참고] Spring Boot OAuth 2.0과 Spring Boot OAuth 2.0 Client를 비교하는 내용입니다.
특징
Spring Boot OAuth 2.0
Spring Boot OAuth 2.0 Client
목적
OAuth 2.0 서비스 제공
OAuth 2.0 서비스 사용
역할
제공자(Provider)
클라이언트(Client)
서버 구축
인증 서버와 리소스 서버 구축 필요
별도 서버 구축 불필요
사용 예시
카카오 로그인 서비스 구현
카카오 로그인 기능 사용
인증 처리
자체적으로 인증 처리
외부 제공자를 통한 인증
리소스 접근
자체 리소스 제공
외부 제공자의 리소스 접근
3. 인증 서버(Authorization Server)와 리소스 서버(Resource Server)
💡 인증 서버(Authorization Server)와 리소스 서버(Resource Server) 💡 인증 서버(Authorization Server) - OAuth 2.0 프로토콜에서 ‘인증’과 ‘권한 부여’를 담당하는 서버입니다. 클라이언트는 사용자의 권한을 인증 서버에 요청하고 인증 서버는 사용자의 동의를 얻어 접근 토큰(Access Token)을 발급합니다. 이 발급받은 접근 토큰(Access Token)을 기반으로 리소스 서버에 접근하여 사용자가 요청한 정보에 대한 응답을 반환해 줍니다. 💡 리소스 서버(Resource Server)
- OAuth 2.0 프로토콜에서 인증 서버로부터 발급받은 액세스 토큰을 사용하여 보호된 리소스에 대한 클라이언트의 요청을 인가하고 응답하는 서버입니다. 보호된 리소스는 사용자 정보와 같은 중요한 데이터를 포함할 수 있습니다.
4. Spring Boot OAuth 2.0 Client의 처리 과정
💡 Spring Boot OAuth 2.0 Client의 처리 과정 - 사용자는 안전하게 자신의 정보에 접근할 수 있는 권한을 클라이언트 애플리케이션에 부여하고, 클라이언트 애플리케이션은 이 권한을 사용하여 리소스 서버로부터 필요한 정보를 얻을 수 있습니다.
0. 사용자가 외부 로그인(kakao, naver, google,..) 로그인 버튼을 누릅니다. 1. 인가 요청(Authorization Request) : Spring Boot OAuth Client API 서버 → 사용자 - Spring Boot OAuth Client API 서버는 사용자에게 인증서버 페이지로 리다이렉트 하여 ‘로그인 페이지를 출력’시켜줍니다.
2. 인가 승인(Authorization Grant) : 사용자 → Spring Boot OAuth Client API 서버 - 사용자가 인증서버 페이지의 로그인을 성공하였을 경우 Spring Boot OAuth Client API 서버로 ‘인증 및 인가 권한을 요청’합니다.
3. 인가 승인(Authorization Grant) : Spring Boot OAuth Client API 서버 → 인증 서버 - Spring Boot OAuth Client API 서버는 인증 서버로 접근 권한을 위한 접근 토큰(Access Token)을 요청합니다.
4. 접근 토큰(Access Token) : 인증 서버 → Spring Boot OAuth Client API 서버 - 인증 서버에서는 사용자를 ‘인증’하고 접근 토큰(Access Token)을 발급하여 API 서버로 응답해 줍니다.
5. 접근 토큰(Access Token) : Spring Boot OAuth Client API 서버 → 리소스 서버 - API 서버는 인증 서버로부터 발급받은 접근 토큰(Access Token)을 기반으로 리소스 서버에 요청을 합니다.
6. 보호된 리소스(Protected Resource) : 리소스 서버 → API 서버 - 리소스 서버는 접근 토큰을 ‘인증’하고 API 서버에 보호된 리소스를 반환해 줍니다. ex) 카카오 로그인을 수행하는 경우, 사용자 이름, 이메일 등을 Spring Boot OAuth Client API 서버로 반환해 줍니다.
- 위에서 발급받은 값을 기반으로 설정 값을 구성하였습니다. - 해당 부분에서 registration.kakao.client-id, client-secret과 registration.naver.client-id, client-secret 값은 각각 발급 받은 정보에 맞게 구성하셔야 합니다.
spring:
security:
oauth2:
client:
# OAuth2 인증 제공자에 대한 설정 정보를 포함합니다.
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: id
# 클라이언트 애플리케이션(Spring Boot)에 대한 설정을 포함합니다.
registration:
kakao:
client-id: a63845846825e5cecaba3a4216cd5f10
client-secret: rZ1KRAS6gS0C7HMCe38NZOqgrMQ2Yxou
redirect-uri: http://localhost:8080/api/v1/oauth2/kakao
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: kakao
scope:
- kakao_account.name
- kakao_account.email
- kakao_account.profile
naver:
client-id: UyfYudFoyg8GY0pfktnq
client-secret: Cnhn0xscZ5
redirect-uri: http://localhost:8080/api/v1/oauth2/naver
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
client-name: naver
scope:
- # 별도의 요청 정보는 사용하지 않음.
2.2. 환경 파일 구조
💡환경 파일 구조
- application-oauth2.yml이라는 환경파일을 구성하였고 application.properties라는 파일을 호출하도록 구성하였습니다.
2.3. application.properties 내에서 호출
💡 application.properties 내에서 호출
- application.properties 내에서 spring.profiles.active 속성으로 불러오는 형태로 구성하였습니다
- 애플리케이션 서버를 실행할 때 아래와 같이 두 개의 환경 파일을 불러옴을 확인하였습니다.
3. [참고] 인증, 리소스 서버 환경설정 정보 객체화
💡 [참고] 인증, 리소스 서버 환경설정 정보 객체화
- 위에서 properties 파일을 구성한 파일들을 쉽게 객체 형태로 불러올 수 있도록, Spring Boot Configuration Processor 라이브러리를 활용하여 객체화합니다. - 이는 추후에 설정 파일 값을 불러오기 위해 사용되며, 복잡성을 줄이기 위해 객체 형태로 구성합니다. 필수 사항이 아니며 기존의 @Value()를 통해서 환경 파일을 불러와도 됩니다.
3.1. OAuthProviderProperties.java
💡 OAuthProviderProperties.java
- application.oauth2.yml 파일 내에서 provider 부분을 구현한 내용을 객체와 매핑하여 불러오는 부분입니다.
package com.adjh.springbootoauth2.config.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.security.oauth2.client.provider")public record OAuth2ProviderProperties(ProviderConfig kakao, ProviderConfig naver){
public record ProviderConfig(
String authorizationUri,
String tokenUri,
String userInfoUri,
String userNameAttribute
){
}
}
3.2. OAuth2RegistrationProperties
💡 OAuth2RegistrationProperties
- application.oauth2.yml 파일 내에서 Registration 부분을 구현한 내용을 객체와 매핑하여 불러오는 부분입니다.
- 카카오/네이버(리소스, 인증 서버)로 외부 통신을 하기 위해 RestTemplate을 활용하기 위해 이를 구성합니다. - 기본헤더로 Content-Type을 JSON으로 설정하고 한글 깨짐 문제를 해결하기 위해 StringHttpMessageConverter를 UTF-8 인코딩으로 추가한 형태로 구성하였습니다.
package com.adjh.springbootoauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
/**
* RestTemplate 구성합니다.
*
* @author : jonghoon
* @fileName : RestTemplateConfig
* @since : 11/1/24
*/publicclassRestTemplateConfig{
/**
* 사전 기본이 되는 RestTemplate 구성합니다.
*
* @return
*/@Beanpublic RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// RestTemplate 이용 중 클라이언트의 한글 깨짐 증상에 대한 수정
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
}
- 오버라이딩받은 kakaoLogin(), naverLogin()이 존재하며, 내부 메서드로 사용되는 defaultHeader, cvtObjectToMap, getKakaoTokenInfo, getKakaoUserInfo, getNaverTokenInfo, getNaverUserInfo로 구성되어 있습니다.
메서드 명
분류
설명
kakaoLogin
주요 메서드
카카오 로그인을 수행하고 사용자 정보를 반환
naverLogin
주요 메서드
네이버 로그인을 수행하고 사용자 정보를 반환
defaultHeader
내부 메서드
기본 HTTP 헤더를 구성하여 반환
cvtObjectToMap
내부 메서드
객체를 Map으로 변환
getKakaoTokenInfo
내부 메서드
카카오 토큰(접근 토큰, 갱신 토큰)을 발급 받음
getKakaoUserInfo
내부 메서드
카카오 로그인 사용자 정보를 가져옴
getNaverTokenInfo
내부 메서드
네이버 토큰(접근 토큰, 갱신 토큰)을 발급 받음
getNaverUserInfo
내부 메서드
네이버 로그인 사용자 정보를 가져옴
package com.adjh.springbootoauth2.service.impl;
import com.adjh.springbootoauth2.config.RestTemplateConfig;
import com.adjh.springbootoauth2.config.properties.OAuth2ProviderProperties;
import com.adjh.springbootoauth2.config.properties.OAuth2RegistrationProperties;
import com.adjh.springbootoauth2.dto.oauth2.*;
import com.adjh.springbootoauth2.service.OAuth2Service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.*;
/**
* OAuth 2.0을 처리하는 서비스 구현체
*
* @author : jonghoon
* @fileName : OAuth2ServiceImpl
* @since : 11/1/24
*/@Slf4j@Service("OAuth2ServiceImpl")publicclassOAuth2ServiceImplimplementsOAuth2Service{
privatefinal RestTemplateConfig restTemplateConfig;
privatefinal OAuth2ProviderProperties oAuthProvider;
privatefinal OAuth2RegistrationProperties oAuthRegistration;
publicOAuth2ServiceImpl(RestTemplateConfig restTemplateConfig, OAuth2ProviderProperties oAuthProvider, OAuth2RegistrationProperties oAuthRegistration){
this.restTemplateConfig = restTemplateConfig;
this.oAuthProvider = oAuthProvider;
this.oAuthRegistration = oAuthRegistration;
}
/**
* 기본적으로 사용하는 Header를 구성하여 반환합니다.
*
* @return
*/private HttpHeaders defaultHeader(){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
return headers;
}
/**
* 제공자(Kakao) 로그인을 수행하고 정보를 반환 받는 서비스입니다.
*
* @param authInfo
* @return
*/@Overridepublic OAuth2KakaoUserInfoDto kakaoLogin(OAuth2AuthInfoDto authInfo){
log.debug("[+] 카카오 로그인이 성공하여 리다이렉트 되었습니다.", authInfo);
log.debug("코드 값 확인 : {}", authInfo.getCode());
log.debug("에러 값 확인 : {}", authInfo.getError());
log.debug("에러 설명 값 확인 : {}", authInfo.getErrorDescription());
log.debug("상태 값 확인 : {}", authInfo.getState());
// [STEP1] 리다이렉트로 반환 받은 인증 코드의 존재여부를 체크합니다.if (authInfo.getCode() == null || authInfo.getCode().isEmpty()) {
log.error("[-] 카카오 로그인 리다이렉션에서 문제가 발생하였습니다.");
returnnull;
}
// [STEP2] 카카오로 토큰을 요청합니다.(접근 토큰, 갱신 토큰)
OAuth2TokenInfoDto kakaoTokenInfo = this.getKakaoTokenInfo(authInfo.getCode());
log.debug("토큰 정보 전체를 확인합니다 :: {}", kakaoTokenInfo);
// [STEP3] 접근 토큰을 기반으로 사용자 정보를 요청합니다.
OAuth2KakaoUserInfoDto userInfo = this.getKakaoUserInfo(kakaoTokenInfo.getAccessToken());
log.debug("userInfo :: {}", userInfo);
return userInfo;
}
/**
* Convert Object To Map
*
* @param obj
* @return
*/private Map<string, object=""> cvtObjectToMap(Object obj) {
ObjectMapper mapper = new ObjectMapper();
return mapper.convertValue(obj, new TypeReference<>() {
});
}
/**
* KAKAO TOKEN 토큰(접근토큰, 갱신토큰)을 발급 받습니다.
*
* @param authCode 인증 코드
* @return OAuth2TokenInfoDto
* @refrence <https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#get-token-info>
*/private OAuth2TokenInfoDto getKakaoTokenInfo(String authCode){
log.debug("[+] getKakaoTokenInfo 함수가 실행 됩니다. :: {}", authCode);
OAuth2TokenInfoDto resultDto = null;
ResponseEntity<map<string, object="">> responseTokenInfo = null;
// [STEP1] 카카오 토큰 URL로 전송할 데이터 구성
MultiValueMap<string, object=""> requestParamMap = new LinkedMultiValueMap<>();
requestParamMap.add("grant_type", "authorization_code");
requestParamMap.add("client_id", oAuthRegistration.kakao().clientId());
requestParamMap.add("redirect_uri", oAuthRegistration.kakao().redirectUri());
requestParamMap.add("code", authCode);
requestParamMap.add("client_secret", oAuthRegistration.kakao().clientSecret());
HttpEntity<multivaluemap<string, object="">> requestMap = new HttpEntity<>(requestParamMap, this.defaultHeader());
try {
// [STEP2] 카카오 토큰 URL로 RestTemplate 이용하여 데이터 전송
responseTokenInfo = restTemplateConfig
.restTemplate()
.exchange(oAuthProvider.kakao().tokenUri(), HttpMethod.POST, requestMap, new ParameterizedTypeReference<>() {
});
} catch (Exception e) {
log.error("[-] 토큰 요청 중에 오류가 발생하였습니다. {}", e.getMessage());
}
// [STEP3] 토큰 반환 값 결과값으로 구성if (responseTokenInfo != null && responseTokenInfo.getBody() != null && responseTokenInfo.getStatusCode().is2xxSuccessful()) {
Map<string, object=""> body = responseTokenInfo.getBody();
if (body != null) {
resultDto = OAuth2TokenInfoDto.builder()
.accessToken(body.get("access_token").toString())
.refreshToken(body.get("refresh_token").toString())
.tokenType(body.get("token_type").toString())
.build();
}
} else {
log.error("[-] 토큰 정보가 존재하지 않습니다.");
}
return resultDto;
}
/**
* 카카오 로그인 사용자 정보를 가져옵니다.
*
* @param accessToken
* @return
* @refrence <https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info>
*/private OAuth2KakaoUserInfoDto getKakaoUserInfo(String accessToken){
log.debug("[+] getKakaoUserInfo을 수행합니다 :: {}", accessToken);
ResponseEntity<map<string, object="">> responseUserInfo = null;
OAuth2KakaoUserInfoDto resultDto = null;
// [STEP1] 필수 요청 Header 값 구성 : ContentType, Authorization
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
headers.add("Authorization", "Bearer " + accessToken); // accessToken 추가// [STEP2] 요청 파라미터 구성 : 원하는 사용자 정보
MultiValueMap<string, object=""> userInfoParam = new LinkedMultiValueMap<>();
ObjectMapper objectMapper = new ObjectMapper();
try {
userInfoParam.add("property_keys", objectMapper.writeValueAsString(oAuthRegistration.kakao().scope())); // 불러올 데이터 조회 (리스트 to 문자열 변환)
} catch (JsonProcessingException e) {
thrownew RuntimeException(e);
}
HttpEntity<multivaluemap<string, object="">> userInfoReq = new HttpEntity<>(userInfoParam, headers);
// [STEP3] 요청 Header, 파라미터를 포함하여 사용자 정보 조회 URL로 요청을 수행합니다.try {
responseUserInfo = restTemplateConfig
.restTemplate()
.exchange(oAuthProvider.kakao().userInfoUri(), HttpMethod.POST, userInfoReq, new ParameterizedTypeReference<>() {
});
log.debug("결과 값 :: {}", responseUserInfo);
} catch (Exception e) {
log.error("[-] 사용자 정보 요청 중에 오류가 발생하였습니다.{}", e.getMessage());
}
// [STEP4] 사용자 정보가 존재한다면 값을 불러와서 OAuth2KakaoUserInfoDto 객체로 구성하여 반환합니다.if (responseUserInfo != null && responseUserInfo.getBody() != null && responseUserInfo.getStatusCode().is2xxSuccessful()) {
Map<string, object=""> body = responseUserInfo.getBody();
if (body != null) {
Map<string, object=""> kakaoAccount = this.cvtObjectToMap(body.get("kakao_account"));
Map<string, object=""> profile = this.cvtObjectToMap(this.cvtObjectToMap(body.get("kakao_account")).get("profile"));
resultDto = OAuth2KakaoUserInfoDto.builder()
.id(body.get("id").toString()) // 사용자 아이디 번호
.statusCode(responseUserInfo.getStatusCode().value()) // 상태 코드
.email(kakaoAccount.get("email").toString()) // 이메일
.profileImageUrl(profile.get("profile_image_url").toString())
.thumbnailImageUrl(profile.get("thumbnail_image_url").toString())
.nickname(profile.get("nickname").toString())
.build();
log.debug("최종 구성 결과 :: {}", resultDto);
}
}
return resultDto;
}
/**
* 제공자(Kakao) 로그인을 수행하고 정보를 반환 받는 서비스입니다.
*
* @param authInfo
* @return
*/@Overridepublic OAuth2NaverUserInfoDto naverLogin(OAuth2AuthInfoDto authInfo){
OAuth2NaverUserInfoDto resultUserInfo = null;
log.debug("[+] 네이버 로그인이 성공하여 리다이렉트 되었습니다.");
log.debug("코드 값 확인2 : {}", authInfo.getCode());
log.debug("에러 값 확인2 : {}", authInfo.getError());
log.debug("에러 설명 값 확인2 : {}", authInfo.getErrorDescription());
log.debug("상태 값 확인2 : {}", authInfo.getState());
// [STEP1] 리다이렉트로 반환 받은 인증 코드의 존재여부를 체크합니다.if (authInfo.getCode() == null || authInfo.getCode().isEmpty()) {
log.error("[-] 카카오 로그인 리다이렉션에서 문제가 발생하였습니다.");
returnnull;
}
// [STEP2] 전달받은 인증코드를 기반으로 토큰정보를 조회합니다.
OAuth2TokenInfoDto naverTokenInfo = this.getNaverTokenInfo(authInfo.getCode(), authInfo.getState());
// [STEP3] 토큰 정보가 존재하는 경우 사용자 정보를 조회합니다.// [STEP3] 접근 토큰을 조회합니다.
String accessToken = naverTokenInfo.getAccessToken();
String refreshToken = naverTokenInfo.getRefreshToken();
log.debug("naverTokenInfo :: {} , {}", accessToken, refreshToken);
resultUserInfo = this.getNaverUserInfo(accessToken);
return resultUserInfo;
}
/**
* NAVER TOKEN 토큰(접근토큰, 갱신토큰)을 발급 받습니다.
*
* @param authCode 인증 코드
* @return OAuth2TokenInfoDto 토큰 결과값
* @refrence : <https://developers.naver.com/docs/login/api/api.md#1--%EC%A4%80%EB%B9%84%EC%82%AC%ED%95%AD>
*/private OAuth2TokenInfoDto getNaverTokenInfo(String authCode, String state){
log.debug("[+] getNaverTokenInfo 함수가 실행 됩니다. :: {}", authCode);
OAuth2TokenInfoDto resultDto = null;
ResponseEntity<map<string, object="">> responseTokenInfo = null;
// [STEP1] 네이버 토큰 URL로 전송할 데이터 구성
MultiValueMap<string, string=""> requestParamMap = new LinkedMultiValueMap<>();
requestParamMap.add("grant_type", "authorization_code"); // 인증 과정에 대한 구분값: 1. 발급:'authorization_code', 2. 갱신:'refresh_token', 3. 삭제: 'delete'
requestParamMap.add("client_id", oAuthRegistration.naver().clientId()); // 애플리케이션 등록 시 발급받은 Client ID 값
requestParamMap.add("client_secret", oAuthRegistration.naver().clientSecret()); // 애플리케이션 등록 시 발급받은 Client secret 값
requestParamMap.add("code", authCode); // 로그인 인증 요청 API 호출에 성공하고 리턴받은 인증코드값 (authorization code)
requestParamMap.add("state", state); // 사이트 간 요청 위조(cross-site request forgery) 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰값으로 URL 인코딩을 적용한 값을 사용
requestParamMap.add("redirect_uri", oAuthRegistration.naver().redirectUri()); // 애플리케이션 등록 시 발급받은 Client secret 값
HttpEntity<multivaluemap<string, string="">> requestMap = new HttpEntity<>(requestParamMap, this.defaultHeader());
try {
// [STEP2] 네이버 토큰 URL로 RestTemplate 이용하여 데이터 전송
responseTokenInfo = restTemplateConfig
.restTemplate()
.exchange(oAuthProvider.naver().tokenUri(), HttpMethod.POST, requestMap, new ParameterizedTypeReference<>() {
});
log.debug("네이버 로그인 결과 :: {}", responseTokenInfo);
} catch (Exception e) {
log.error("[-] 토큰 요청 중에 오류가 발생하였습니다.{}", e.getMessage());
}
// [STEP3] 토큰 반환 값 결과값으로 구성if (responseTokenInfo != null && responseTokenInfo.getBody() != null && responseTokenInfo.getStatusCode().is2xxSuccessful()) {
Map<string, object=""> body = responseTokenInfo.getBody();
if (body != null) {
resultDto = OAuth2TokenInfoDto.builder()
.accessToken(body.get("access_token").toString())
.refreshToken(body.get("refresh_token").toString())
.tokenType(body.get("token_type").toString())
.expiresIn(body.get("expires_in").toString())
.build();
}
} else {
log.error("[-] 토큰 정보가 존재하지 않습니다.");
}
log.debug("최종 결과 값을 확인합니다 : {}", resultDto.toString());
return resultDto;
}
/**
* Naver의 사용자 정보를 조회합니다.
*
* @param accessToken
* @return
* @refrence <https://developers.naver.com/docs/login/profile/profile.md>
*/private OAuth2NaverUserInfoDto getNaverUserInfo(String accessToken){
log.debug("[+] getNaverUserInfo 함수를 수행합니다 :: {}", accessToken);
ResponseEntity<map<string, object="">> responseUserInfo = null;
OAuth2NaverUserInfoDto resultDto = null;
// [STEP1] 필수 요청 Header 값 구성 : ContentType, Authorization
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", "Bearer " + accessToken); // accessToekn 추가// [STEP2] 요청 파라미터 구성 : 별도의 요청정보는 없음.
MultiValueMap<string, object=""> userInfoParam = new LinkedMultiValueMap<>();
HttpEntity<multivaluemap<string, object="">> userInfoReq = new HttpEntity<>(userInfoParam, headers);
log.debug("요청 값 :: {}", userInfoReq);
// [STEP3] 요청 Header, 파라미터를 포함하여 사용자 정보 조회 URL로 요청을 수행합니다.try {
responseUserInfo = restTemplateConfig
.restTemplate()
.exchange(oAuthProvider.naver().userInfoUri(), HttpMethod.POST, userInfoReq, new ParameterizedTypeReference<>() {
});
} catch (Exception e) {
log.error("[-] 사용자 정보 요청 중에 오류가 발생하였습니다.{}", e.getMessage());
}
log.debug("사용자 조회 :: {}", responseUserInfo);
// [STEP4] 사용자 정보가 존재한다면 값을 불러와서 OAuth2NaverUserInfoDto 객체로 구성하여 반환합니다.if (responseUserInfo != null && responseUserInfo.getBody() != null && responseUserInfo.getStatusCode().is2xxSuccessful()) {
Map<string, object=""> body = responseUserInfo.getBody();
if (body != null && body.get("response") != null) {
Map<string, object=""> resBody = this.cvtObjectToMap(body.get("response"));
resultDto = OAuth2NaverUserInfoDto.builder()
.resultCode(body.get("resultcode").toString())
.message(body.get("message").toString())
.response(
OAuth2NaverUserInfoDto.NaverUserResponse
.builder()
.id(resBody.get("id").toString())
.email(resBody.get("email").toString())
.name(resBody.get("name").toString())
.nickname(resBody.get("nickname").toString())
.build())
.build();
log.debug("userInfo :: {}", resultDto);
}
}
return resultDto;
}
}
</string,></string,></multivaluemap<string,></string,></map<string,></string,></multivaluemap<string,></string,></map<string,></string,></string,></string,></multivaluemap<string,></string,></map<string,></string,></multivaluemap<string,></string,></map<string,></string,>
3.4. 처리 결과 확인 : 카카오로그인
💡 처리 결과 확인 : 카카오로그인
- 아래와 같이 작업해 둔 콘솔을 통해서 확인할 수 있습니다.
1. 첫 번째 : kakaoLogin() 메서드가 수행하면서 auth code를 발급받았습니다. 2. 두 번째 : getKakaoTokenInfo() 메서드가 수행하면서 accessToken, refreshTokn을 발급받았습니다. 3. 세 번째 : getKakaoUserInfo() 메서드가 수행하면서 사용자 정보를 반환받았습니다.
💡 결과론적으로 아래와 같은 사용자 정보를 얻었습니다.
3.5. 처리 결과 확인 : 네이버 로그인
💡 처리 결과 확인 : 네이버 로그인
- 아래와 같이 작업해 둔 콘솔을 통해서 확인할 수 있습니다.
1. 첫 번째 : naverLogin() 메서드가 수행하면서 auth code를 발급받았습니다. 2. 두 번째 : getNaverTokenInfo() 메서드가 수행하면서 accessToken, refreshTokn을 발급받았습니다. 3. 세 번째 : getNaverUserInfo() 메서드가 수행하면서 사용자 정보를 반환받았습니다.