Java/Spring Boot
[Java] Spring Boot 3.x Security + OAuth 2.0 Client 이해하고 적용하기 -1 : 초기 환경 구성 및 카카오, 네이버 로그인 사용자 정보 조회
adjh54
2024. 11. 9. 15:29
반응형
해당 글에서는 Spring Boot 3.x 기반 개발 환경에서 Security + OAuth 2.0을 활용하여 초기 환경을 설정하고 외부 로그인을 통해 사용자 정보를 조회하는 과정에 대해 알아봅니다.
💡 [참고] Spring Boot Security + JWT와 OAuth2 관련되어서 궁금하시다면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 상세 분류 | 주제 | 링크 |
Spring Boot 2.x | 이론 | Spring Boot Security 이해하기 -1 : 2.7.x 버전 구조 및 파일 이해 | https://adjh54.tistory.com/91 |
Spring Boot 2.x | 환경 설정 | Spring Boot Security 이해하기 -2 : 2.7.x 버전 구현하기 | https://adjh54.tistory.com/92 |
Spring Boot 2.x | 이론 | Spring Boot Security 이해하기 -3: JWT(JSON Web Token) 이해하기 | https://adjh54.tistory.com/93 |
Spring Boot 2.x | 환경 설정 | Spring Boot Security 이해하기 -4: JWT 환경 설정 및 구성 하기 | https://adjh54.tistory.com/94 |
Spring Boot 2.x | 소스코드 | Spring Boot Security 2.x + JWT 기반 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot2-security |
기타 | 이론 | Spring Boot 2.x.x 버전 프로젝트 생성: 지원 종료 및 다운그레이드 | https://adjh54.tistory.com/361 |
Spring Boot 3.x | 이론 | Spring Boot Security 3.x + JWT 이해하기 -1 : 구조 및 Client / Server 처리과정 | https://adjh54.tistory.com/576 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -2 : 환경 설정 및 구성 | https://adjh54.tistory.com/577 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -3 : Refresh Token 활용 | https://adjh54.tistory.com/583 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -4 : 로그아웃 + 블랙리스트 활용 | https://adjh54.tistory.com/592 |
Spring Boot 3.x | 소스코드 | Spring Boot Security 3.x + JWT 기반 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot3-security |
React | 소스코드 | Spring Boot 3.x와 통신하여 로그인을 수행하는 클라이언트 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/react-login |
Spring Boot 2.x | 이론 | Spring Boot OAuth 2 Client 이해하기 -1 : 정의, 흐름, 인증방식 종류 | https://adjh54.tistory.com/221 |
Spring Boot 2.x | 환경 설정 | Spring Boot OAuth 2 Client 이해하기 -2: Security 없이 카카오 로그인 구성 | https://adjh54.tistory.com/224 |
Spring Boot 2.x | 환경 설정 | Spring Boot OAuth2 Client + Spring Security + JWT + Kakao 구성하기 -1 : 초기 환경 | https://adjh54.tistory.com/237 |
Spring Boot 3.x | 환경 설정 | [Java] Spring Boot 3.x Security + OAuth 2.0 Client 이해하고 적용하기 -1 : 초기 환경 구성 및 카카오, 네이버 로그인 사용자 정보 조회 | https://adjh54.tistory.com/594 |
Spring Boot 3.x | 소스코드 | Spring Boot Security 3.x + JWT + OAuth 2.0 기반 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot3-security-oauth2 |
React | 소스코드 | Spring Boot 3.x와 통신하여 로그인 + 외부 로그인(카카오, 네이버)을 수행하는 클라이언트 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/react-login |
기타 | 환경 설정 | Spring Boot 환경에서 OAuth 2.0 설정 -1: 카카오 로그인 설정 및 구성요소 확인 | https://adjh54.tistory.com/590 |
기타 | 환경 설정 | Spring Boot 환경에서 OAuth 2.0 설정 -2 : 네이버 로그인 설정 및 구성요소 확인 | https://adjh54.tistory.com/591 |
1) Spring Boot OAuth 2.0 Client
💡Spring Boot OAuth 2.0 Client
- 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 서버로 반환해 줍니다.
반응형
2) 환경 구성
💡 환경 구성
- 해당 환경 구성은 이전에 작성한 Spring Boot 3.x Security + JWT 기반 환경에서 이어서 구성이 되었습니다.
- 아래의 글을 참고하여 구성되었다는 가정하에 아래의 환경을 구성합니다.
분류 | 상세 분류 | 주제 | 링크 |
Spring Boot 3.x | 이론 | Spring Boot Security 3.x + JWT 이해하기 -1 : 구조 및 Client / Server 처리과정 | https://adjh54.tistory.com/576 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -2 : 환경 설정 및 구성 | https://adjh54.tistory.com/577 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -3 : Refresh Token 활용 | https://adjh54.tistory.com/583 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -4 : 로그아웃 + 블랙리스트 활용 | https://adjh54.tistory.com/592 |
Spring Boot 3.x | 소스코드 | Spring Boot Security 3.x + JWT 기반 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot3-security |
1. 라이브러리 추가
💡라이브러리 추가
- 해당 환경에서는 Spring Boot 3.3.5 기준으로 구성을 하였습니다.
라이브러리 | 설명 |
spring-boot-starter-web | API 통신을 위해 사용합니다. |
spring-boot-starter-security | 인증과 인가를 통한 로그인 작업을 위해 사용합니다. |
spring-boot-starter-oauth2-client | 외부 로그인을 구현하기 위해 사용합니다. |
spring-boot-starter-data-redis | 토큰 블랙리스트를 구현하기 위해 사용합니다. |
spring-boot-configuration-processor | 환경 변수를 객체화하여 사용하기 위해 사용합니다. |
mybatis-spring-boot-starter | 데이터베이스와의 접근을 위한 목적으로 SQL Mapper MyBatis를 사용합니다. |
io.jsonwebtoken:jjwt | JSON Web Token을 통한 인가를 적용하기 위해 사용합니다. |
org.projectlombok:lombok | 어노테이션 기반 코드 다이어트를 위한 목적으로 사용합니다. |
org.postgresql:postgresql | 데이터베이스 내의 사용자 정보를 이용하기 위한 목적으로 사용합니다. |
ext {
set('bootVer', "3.3.5")
set('jacksonVer', "2.17.2")
}
dependencies {
// [Spring Boot Starter]
implementation "org.springframework.boot:spring-boot-starter-web:${bootVer}" // Spring Boot Web
implementation "org.springframework.boot:spring-boot-starter-log4j2:${bootVer}" // Spring Boot Log4j2
implementation "org.springframework.boot:spring-boot-starter-security:${bootVer}" // Spring Boot Security
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:${bootVer}" // Spring Boot OAuth 2.0 Client
implementation "org.springframework.boot:spring-boot-starter-data-redis:${bootVer}" // Spring Boot Data Redis
implementation "org.springframework.boot:spring-boot-configuration-processor:${bootVer}" // Spring Boot Configuration Processor
// [OpenSource]
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' // MyBatis
implementation 'io.jsonwebtoken:jjwt:0.12.6' // JSON-WEB-TOKEN
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' // DataType Converter
implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVer}" // Jackson Data Binding
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jacksonVer}" // Jackson DatabindFormat Yaml
// [compile & runtime & test]
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
}
2. 인증, 리소스 서버 환경설정
💡 인증, 리소스 서버 환경설정
- 아래의 카카오와 네이버 로그인을 위한 클라이언트 아이디와 클라이언트 시크릿 발급이 필요합니다. 이에 대한 설정은 아래의 글을 확인하시면 됩니다.
2.1. application.oauth2.yml
💡 application.oauth2.yml
- 위에서 발급받은 값을 기반으로 설정 값을 구성하였습니다.
- 해당 부분에서 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 속성으로 불러오는 형태로 구성하였습니다
💡 [참고] 해당 설정방법에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
2.4. 로드 확인
💡 로드 확인
- 애플리케이션 서버를 실행할 때 아래와 같이 두 개의 환경 파일을 불러옴을 확인하였습니다.
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 부분을 구현한 내용을 객체와 매핑하여 불러오는 부분입니다.
package com.adjh.springbootoauth2.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration")
public record OAuth2RegistrationProperties(
RegistrationConfig kakao,
RegistrationConfig naver
) {
public record RegistrationConfig(
String clientId,
String clientSecret,
String redirectUri,
String authorizationGrantType,
String clientAuthenticationMethod,
String clientName,
List<String> scope
) {
}
}
3.3. xxApplication.java
💡 xxApplication.java
- 애플리케이션을 불러올 때 @ConfigurationPropertiesScan 어노테이션을 통해 @ConfigurationProperties로 지정한 파일들에 대해 불러옵니다.
package com.adjh.springbootoauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan("com.adjh.springbootoauth2.config.properties")
public class SpringBoot3SecurityOauth2Application {
public static void main(String[] args) {
SpringApplication.run(SpringBoot3SecurityOauth2Application.class, args);
}
}
💡 [참고] 해당 어노테이션에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
3. RestTemplate 구성
💡 RestTemplate 구성
- 카카오/네이버(리소스, 인증 서버)로 외부 통신을 하기 위해 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
*/
public class RestTemplateConfig {
/**
* 사전 기본이 되는 RestTemplate 구성합니다.
*
* @return
*/
@Bean
public 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;
}
}
💡 [참고] 이전에 작성한 글을 기반으로 RestTemplate을 구성하였습니다.
3) 프로세스 별 처리 구성
💡 프로세스 별 처리 구성
- 아래의 이미지대로 순차적으로 프로세스 및 처리 과정을 구성해 봅니다.
1. 로그인 요청, 소셜 로그인 페이지로 리다이렉트 : React
💡 로그인 요청, 소셜 로그인 페이지로 리다이렉트 : React
- 사용자가 ‘외부 로그인’ 버튼을 누르면, 이에 대한 로그인 페이지를 출력하는 과정입니다.
1.1. OauthLoginComponent
💡 OauthLoginComponent
- 아래와 같은 화면이 출력되도록 간단한 UI를 구성하였습니다.
- 이미지를 클릭하면 각각 인증을 위한 URL에 클라이언트 아이디와 리다이렉트 URL을 쿼리 스트링(GET 방식 파라미터)으로 전송을 하여 로그인 페이지를 출력을 합니다.
const OauthLoginComponent = () => {
const oAuthLoginHandler = (() => {
return {
/**
* 카카오 로그인
*/
kakao: () => {
const kakaoAuthUrl = process.env.REACT_APP_API_OAUTH2_KAKAO_AUTH_URL;
const kakaoClientId = process.env.REACT_APP_API_OAUTH2_KAKAO_CLIENT_ID;
const kakaoRedirectUrl = process.env.REACT_APP_API_OAUTH2_KAKAO_REDIRECT_URL;
window.location.href = `${kakaoAuthUrl}?client_id=${kakaoClientId}&redirect_uri=${kakaoRedirectUrl}&response_type=code`;
},
/**
* 네이버 로그인
*/
naver: () => {
const naverAuthUrl = process.env.REACT_APP_API_OAUTH2_NAVER_AUTH_URL;
const naverClientId = process.env.REACT_APP_API_OAUTH2_NAVER_CLIENT_ID;
const naverRedirectUrl = process.env.REACT_APP_API_OAUTH2_NAVER_REDIRECT_URL;
window.location.href = `${naverAuthUrl}?client_id=${naverClientId}&redirect_uri=${naverRedirectUrl}&response_type=code&state=RANDOM_STATE`;
},
};
})();
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<div style={{ marginBottom: 100 }}>
<h1 style={{ textAlign: 'center' }}>OAuth 2.0 기반 소셜 로그인</h1>
</div>
<div
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}}>
<div style={{}}>
<img
style={{ width: 150, height: 40, marginRight: 40 }}
src={'assets/icons/kakao_login_medium_narrow.png'}
onClick={oAuthLoginHandler.kakao}
alt='카카오'
/>
<img
style={{ width: 150, height: 40 }}
src={'assets/icons/btnG_complete_type.png'}
onClick={oAuthLoginHandler.naver}
alt='네이버'
/>
</div>
</div>
</div>
);
};
export default OauthLoginComponent;
💡 .env 파일
# 카카오 로그인
REACT_APP_API_OAUTH2_KAKAO_AUTH_URL = <https://kauth.kakao.com/oauth/authorize>
REACT_APP_API_OAUTH2_KAKAO_CLIENT_ID = xx
REACT_APP_API_OAUTH2_KAKAO_REDIRECT_URL =
# 네이버 로그인
REACT_APP_API_OAUTH2_NAVER_AUTH_URL = <https://nid.naver.com/oauth2.0/authorize>
REACT_APP_API_OAUTH2_NAVER_CLIENT_ID = xxx
REACT_APP_API_OAUTH2_NAVER_REDIRECT_URL =
1.2. 로그인 페이지 출력
💡 로그인 페이지 출력
- 인증 URL에 클라이언트 아이디와 리다이렉트 URL을 쿼리 스트링으로 보내면 아래와 같은 로그인 화면이 출력됩니다.
2. 로그인 수행 이후 인증 코드(Auth Code) 반환
💡 로그인 수행 이후 인증 코드(Auth Code) 반환
- 아래의 과정에서는 위에 로그인을 수행하였을 때, 로그인이 성공하면 ‘리다이렉트 URL’로 호출이 됩니다.
- 해당 호출이 될 때 파라미터 인증 코드 값이 반환이 되는데 해당 과정에 대해 확인합니다.
2.1. 반환받을 DTO 구성
💡 반환받을 DTO 구성
- Redirect URL을 통해서 인증 코드와 각각 상태가 담긴 결과가 지정되어 있기에 각각 DTO로 구성하였습니다.
- 각각 반환되는 값은 아래의 공식사이트를 확인할 수 있습니다.
package com.adjh.springbootoauth2.dto.oauth2;
import lombok.Builder;
import lombok.Getter;
/**
* OAUth 2.0 Redirect로 반환 받는 DTO
*
* @author : jonghoon
* @fileName : OAuth2AuthInfoDto
* @since : 11/8/24
*/
@Getter
public class OAuth2AuthInfoDto {
private String code; // 토큰 받기 요청에 필요한 인가 코드
private String error; // 인증 실패 시 반환되는 에러 코드
private String errorDescription; // 인증 실패 시 반환되는 에러 메시지
private String state; // 요청 시 전달한 state 값과 동일한 값
@Builder
public OAuth2AuthInfoDto(String code, String error, String errorDescription, String state) {
this.code = code;
this.error = error;
this.errorDescription = errorDescription;
this.state = state;
}
}
2.2. OAuth2Controller
💡 OAuth2Controller
- 이전 Auth URL의 파라미터로 전달하였던 엔드포인트를 기반으로 Controller에서 이를 매핑해 줍니다.
- 각각의 가이드 문서를 확인해 보면, 반환해 주는 값을 명시해주고 있습니다.
💡 Client에서 구성했던 Redirect URL
package com.adjh.springbootoauth2.controller;
import com.adjh.springbootoauth2.dto.oauth2.OAuth2KakaoUserInfoDto;
import com.adjh.springbootoauth2.dto.oauth2.OAuth2NaverUserInfoDto;
import com.adjh.springbootoauth2.dto.oauth2.OAuth2AuthInfoDto;
import com.adjh.springbootoauth2.service.OAuth2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* OAuth 2.0 Controller
*
* @author : jonghoon
* @fileName : OAuth2Controller
* @since : 11/1/24
*/
@Slf4j
@RestController
@RequestMapping("api/v1/oauth2")
public class OAuth2Controller {
private final OAuth2Service oAuth2Service;
public OAuth2Controller(OAuth2Service oAuth2Service) {
this.oAuth2Service = oAuth2Service;
}
/**
* [API] 카카오톡 로그인 : Redirect URL
*
* @param code 토큰 받기 요청에 필요한 인가 코드
* @param error 인증 실패 시 반환되는 에러 코드
* @param error_description 인증 실패 시 반환되는 에러 메시지
* @param state 요청 시 전달한 state 값과 동일한 값
* @return ApiResponseWrapper : 응답 결과 및 응답 코드 반환
* @Refrence : <https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#kakaologin>
*/
@GetMapping("/kakao")
public ResponseEntity kakaoLogin(
@RequestParam(required = false) String code,
@RequestParam(required = false) String error,
@RequestParam(required = false) String error_description,
@RequestParam(required = false) String state
) {
log.debug("Kakao Login - code: {}, error: {}, error_description: {}, state: {}", code, error, error_description, state);
OAuth2AuthInfoDto kakaoReqDto = OAuth2AuthInfoDto.builder()
.code(code)
.error(error)
.errorDescription(error_description)
.state(state)
.build();
OAuth2KakaoUserInfoDto resultObj = oAuth2Service.kakaoLogin(kakaoReqDto);
return new ResponseEntity<>(resultObj, HttpStatus.OK);
}
/**
* [API] 네이버 로그인 : : Redirect URL
*
* @param code 네이버 로그인 인증에 성공하면 반환받는 인증 코드, 접근 토큰(access token) 발급에 사용
* @param error 네이버 로그인 인증에 실패하면 반환받는 에러 코드
* @param error_description 네이버 로그인 인증에 실패하면 반환받는 에러 메시지
* @param state 사이트 간 요청 위조 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰으로 URL 인코딩을 적용한 값
* @return ApiResponseWrapper : 응답 결과 및 응답 코드 반환
* @refrence : <https://developers.naver.com/docs/login/api/api.md#4-1--%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D-%EC%9A%94%EC%B2%AD>
*/
@GetMapping("/naver")
public ResponseEntity naverLogin(
@RequestParam(required = false) String code,
@RequestParam(required = false) String error,
@RequestParam(required = false) String error_description,
@RequestParam(required = false) String state
) {
log.debug("Naver Login - code: {}, error: {}, error_description: {}, state: {}", code, error, error_description, state);
OAuth2AuthInfoDto naverReqDto = OAuth2AuthInfoDto.builder()
.code(code)
.error(error)
.errorDescription(error_description)
.state(state)
.build();
OAuth2NaverUserInfoDto resultObj = oAuth2Service.naverLogin(naverReqDto);
return new ResponseEntity<>(resultObj, HttpStatus.OK);
}
}
3.2. 결과값 확인
💡 결과값 확인
- 각각 code라는 값으로 code(인가코드)를 반환받았습니다.
3. 인증 코드(Auth Code) 기반 토큰(접근 토큰, 갱신 토큰) 값 반환 및 토큰 기반 사용자 정보 조회
💡 인증 코드(Auth Code) 기반 토큰(접근 토큰, 갱신 토큰) 값 반환 및 토큰 기반 사용자 정보 조회
1. Redirect URL을 통해서 전달받은 결과 값인 인증 코드(Auth Code)를 기반으로 인증/리소스 서버와의 통신을 통해 리소스에 접근을 위한 토큰(접근 토큰, 갱신 토큰)을 반환받습니다.
2. 발급받은 토큰(접근 토큰, 갱신 토큰)을 기반으로 인증/리소스 서버와의 통신을 통해 사용자 정보를 조회합니다.
3.1. 응답값 DTO 구성
💡 응답값 DTO 구성
- RestTemplate을 통해 인증서버/리소스 서버로부터 통신한 반환값에 대해 DTO로 정의하였습니다.
객체 | 설명 |
OAuth2AuthInfoDto | 리다이렉트 URL로 반환되는 응답 값들을 정의한 DTO. 인증 코드, 에러 코드, 에러 설명, 상태 값 등을 포함 |
OAuth2TokenInfoDto | 인증 코드(Auth Code)를 기반으로 반환되는 토큰 관련 정보를 정의한 DTO. 접근 토큰, 갱신 토큰, 토큰 타입, 만료 시간 등의 정보를 포함 |
OAuth2KakaoUserInfoDto | 카카오 로그인 시 접근 토큰(AccessToken)을 기반으로 조회한 사용자 정보를 정의한 DTO. 사용자 ID, 이메일, 닉네임, 프로필 이미지 URL 등의 정보를 포함 |
OAuth2NaverUserInfoDto | 네이버 로그인 시 접근 토큰(AccessToken)을 기반으로 조회한 사용자 정보를 정의한 DTO. 네이버에서 제공하는 사용자 정보를 포함 |
💡 OAuth2AuthInfoDto
- 리다이렉트 URL로 반환되는 응답 값들에 대해 정의합니다.
package com.adjh.springbootoauth2.dto.oauth2;
import lombok.*;
/**
* OAUth 2.0 Redirect로 반환 받는 DTO
*
* @author : jonghoon
* @fileName : OAuth2AuthInfoDto
* @since : 11/8/24
*/
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OAuth2AuthInfoDto {
private String code; // 토큰 받기 요청에 필요한 인가 코드
private String error; // 인증 실패 시 반환되는 에러 코드
private String errorDescription; // 인증 실패 시 반환되는 에러 메시지
private String state; // 요청 시 전달한 state 값과 동일한 값
@Builder
public OAuth2AuthInfoDto(String code, String error, String errorDescription, String state) {
this.code = code;
this.error = error;
this.errorDescription = errorDescription;
this.state = state;
}
}
💡 OAuth2TokenInfoDto
- Auth Code 값을 기반으로 반환되는 Token과 관련된 정보들을 객체로 정의하였습니다.
package com.adjh.springbootoauth2.dto.oauth2;
import lombok.*;
/**
* OAuth 2.0 토큰 반환 값을 정의한 DTO
*
* @author : jonghoon
* @fileName : OAuth2TokenInfoDto
* @since : 11/9/24
*/
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OAuth2TokenInfoDto {
private String accessToken;
private String refreshToken;
private String tokenType;
private String expiresIn;
private String error;
private String errorDescription;
@Builder
public OAuth2TokenInfoDto(String accessToken, String refreshToken, String tokenType, String expiresIn, String error, String errorDescription) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.tokenType = tokenType;
this.expiresIn = expiresIn;
this.error = error;
this.errorDescription = errorDescription;
}
}
💡 OAuth2KakaoUserInfoDto
- 토큰 정보에서 반환된 접근토큰(AccessToken)을 기반으로 조회한 ‘카카오’의 사용자 정보들을 객체로 정의하였습니다.
package com.adjh.springbootoauth2.dto.oauth2;
import lombok.*;
/**
* Please explain the class!!
*
* @author : jonghoon
* @fileName : OAuth2KakaoUserInfoDto
* @since : 11/8/24
*/
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OAuth2KakaoUserInfoDto {
private String id;
private int statusCode; // 상태 코드
private String email; // 이메일
private String nickname; // 닉네임
private String profileImageUrl; // 프로필 이미지 URL
private String thumbnailImageUrl; // 썸네일 이미지 URL
@Builder
public OAuth2KakaoUserInfoDto(String id, int statusCode, String email, String nickname, String profileImageUrl, String thumbnailImageUrl) {
this.id = id;
this.statusCode = statusCode;
this.email = email;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
this.thumbnailImageUrl = thumbnailImageUrl;
}
}
💡 OAuth2NaverUserInfoDto
- 토큰 정보에서 반환된 접근토큰(AccessToken)을 기반으로 조회한 ‘카카오’의 사용자 정보들을 객체로 정의하였습니다.
package com.adjh.springbootoauth2.dto.oauth2;
import lombok.*;
/**
* 카카오 로그인 반환 사용자 정보
*
* @author : jonghoon
* @fileName : OAuth2NaverUserInfoDto
* @since : 11/3/24
*/
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OAuth2NaverUserInfoDto {
private String resultCode;
private String message;
private NaverUserResponse response;
@Getter
@Builder
@ToString
public static class NaverUserResponse {
private String id;
private String nickname;
private String email;
private String name;
}
@Builder
public OAuth2NaverUserInfoDto(String resultCode, String message, NaverUserResponse response) {
this.resultCode = resultCode;
this.message = message;
this.response = response;
}
}
3.2. OAuthService(interface)
💡 OAuthService(interface)
- Controller에서 해당 인터페이스를 호출하여 비즈니스 로직 처리를 수행합니다.
package com.adjh.springbootoauth2.service;
import com.adjh.springbootoauth2.dto.oauth2.OAuth2KakaoUserInfoDto;
import com.adjh.springbootoauth2.dto.oauth2.OAuth2NaverUserInfoDto;
import com.adjh.springbootoauth2.dto.oauth2.OAuth2AuthInfoDto;
import org.springframework.stereotype.Service;
/**
* OAuth 2.0 기반 로그인 수행
*
* @author : jonghoon
* @fileName : OAuth2Service
* @since : 11/1/24
*/
@Service("OAuth2Service")
public interface OAuth2Service {
OAuth2KakaoUserInfoDto kakaoLogin(OAuth2AuthInfoDto oAuth2AuthInfoDto);
OAuth2NaverUserInfoDto naverLogin(OAuth2AuthInfoDto oAuth2AuthInfoDto);
}
3.3. OAuth2ServiceImpl(인터페이스 구현체)
💡 OAuth2ServiceImpl(인터페이스 구현체)
- 카카오 로그인, 네이버 로그인에 대한 구현체입니다.
- 오버라이딩받은 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")
public class OAuth2ServiceImpl implements OAuth2Service {
private final RestTemplateConfig restTemplateConfig;
private final OAuth2ProviderProperties oAuthProvider;
private final OAuth2RegistrationProperties oAuthRegistration;
public OAuth2ServiceImpl(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
*/
@Override
public 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("[-] 카카오 로그인 리다이렉션에서 문제가 발생하였습니다.");
return null;
}
// [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) {
throw new 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
*/
@Override
public 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("[-] 카카오 로그인 리다이렉션에서 문제가 발생하였습니다.");
return null;
}
// [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() 메서드가 수행하면서 사용자 정보를 반환받았습니다.
💡 결과론적으로 아래와 같은 사용자 정보를 얻었습니다.
오늘도 감사합니다 😀
반응형