[프로젝트] 카카오 로그인 OAuth2 구현 (2024)

카카오클라우드스쿨 인 제주

[프로젝트] 카카오 로그인 OAuth2 구현

끼끼 2024. 8. 14. 23:16

URL 복사 이웃추가

본문 기타 기능

신고하기

블로그랑 다른 팀원 분들의 도움을 받고 구현한 카카오 로그인

보안을 위해 가장 상위의 위치에 .env 파일 만들어서 kakao-client 값을 넣었고 .gitignore.env를 포함시켰다

카카오 클라이언트 값은 REST API 키를 넣는건데, 이걸 구하는 방법은 카카오 디벨로퍼 애플리케이션을 먼저 새로 만들어야 한다 그리고 내 애플리케이션 -> 앱 -> 앱 키 순으로 가면 키 값이 나온다

필요한 키를 복사해서 .env 파일에 PRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID=${REST API 키}를 입력했다

키 값은 어디에도 유출되면 안 되므로 아주 조심해야 한다

카카오 인증 절차를 거치려면 이메일 동의를 받아야되는데, 동의를 받기 위해서 애플리케이션을 비즈니스 앱으로 전환해야 한다

비즈니스 앱으로 전환하는 방법은 프로필 사진 아무거나 등록하면 되었다

카카오 OAuth 로그인 구현한 코드

build.gradle에 플러그인과 의존성 주입한 코드

plugins {id 'java'id 'org.springframework.boot' version '3.2.9-SNAPSHOT'id 'io.spring.dependency-management' version '1.1.6'}group = 'com.example'version = '0.0.1-SNAPSHOT'java {toolchain {languageVersion = JavaLanguageVersion.of(17)}}configurations {compileOnly {extendsFrom annotationProcessor}}repositories {mavenCentral()maven { url 'https://repo.spring.io/snapshot' }}dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'compileOnly 'org.projectlombok:lombok'developmentOnly 'org.springframework.boot:spring-boot-devtools'annotationProcessor 'org.projectlombok:lombok'implementation 'org.springframework.boot:spring-boot-starter-data-jpa'implementation 'org.springframework.boot:spring-boot-starter-webflux'runtimeOnly 'com.mysql:mysql-connector-j'testImplementation 'org.springframework.boot:spring-boot-starter-test'testRuntimeOnly 'org.junit.platform:junit-platform-launcher'implementation 'io.github.cdimascio:dotenv-java:2.2.0'// 카카오 로그인에 필요한 의존성 주입implementation 'org.springframework.boot:spring-boot-starter-security'implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'}tasks.named('test') {useJUnitPlatform()}

application.yml

spring: application: name: sumda profiles: active: local include: my datasource: url: jdbc:mysql://localhost:3306/sumda # MySQL 사용 username: ${DB_USERNAME} password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 드라이버 클래스 네임 수정 jpa: hibernate: ddl-auto: none # 스키마 자동 생성 옵션을 none으로 설정 show-sql: true database-platform: org.hibernate.dialect.MySQLDialect # MySQL Dialect로 변경 security: oauth2: client: registration: kakao: client_id: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID:default_client_id} provider: kakao client-authentication-method: POST authorization-grant-type: authorization_code redirect-uri: "http://localhost:3000/auth" scope: profile_nickname, profile_image, account_email client-name: Kakao 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

Application (메인)

package com.example.sumda;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import io.github.cdimascio.dotenv.Dotenv;@SpringBootApplicationpublic class SumdaApplication {public static void main(String[] args) {Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();System.out.println("KAKAO_CLIENT_ID: " + dotenv.get("SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID"));SpringApplication.run(SumdaApplication.class, args);}}

controller 패키지

KakaoLoginController

package com.example.sumda.controller;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import com.example.sumda.dto.KakaoUserInfoResponseDto;import com.example.sumda.service.KakaoService;import java.io.IOException;@Slf4j@RestController@RequiredArgsConstructor@RequestMapping("")public class KakaoLoginController { private final KakaoService kakaoService; @GetMapping("/callback") public ResponseEntity<?> callback(@RequestParam("code") String code) throws IOException { String accessToken = kakaoService.getAccessTokenFromKakao(code); KakaoUserInfoResponseDto userInfo = kakaoService.getUserInfo(accessToken); // User 로그인, 또는 회원가입 로직 추가 return new ResponseEntity<>(HttpStatus.OK); }}

KakaoLoginPageController

package com.example.sumda.controller;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;@Controller@RequestMapping("/login")public class KakaoLoginPageController { @Value("${spring.security.oauth2.client.registration.kakao.client_id}") private String client_id; @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") private String redirect_uri; @GetMapping("/page") public String loginPage(Model model) { String location = "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id="+client_id+"&redirect_uri="+redirect_uri; model.addAttribute("location", location); return "login"; }}

dto 패키지

KakaoTokenResponseDto

package com.example.sumda.dto;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;import com.fasterxml.jackson.annotation.JsonProperty;import lombok.Getter;import lombok.NoArgsConstructor;@Getter@NoArgsConstructor //역직렬화를 위한 기본 생성자@JsonIgnoreProperties(ignoreUnknown = true)public class KakaoTokenResponseDto { @JsonProperty("token_type") public String tokenType; @JsonProperty("access_token") public String accessToken; @JsonProperty("id_token") public String idToken; @JsonProperty("expires_in") public Integer expiresIn; @JsonProperty("refresh_token") public String refreshToken; @JsonProperty("refresh_token_expires_in") public Integer refreshTokenExpiresIn; @JsonProperty("scope") public String scope;}

KakaoUserInfoResponseDto

package com.example.sumda.dto;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;import com.fasterxml.jackson.annotation.JsonProperty;import lombok.Getter;import lombok.NoArgsConstructor;import java.util.Date;import java.util.HashMap;@Getter@NoArgsConstructor //역직렬화를 위한 기본 생성자@JsonIgnoreProperties(ignoreUnknown = true)public class KakaoUserInfoResponseDto { //회원 번호 @JsonProperty("id") public Long id; //자동 연결 설정을 비활성화한 경우만 존재. //true : 연결 상태, false : 연결 대기 상태 @JsonProperty("has_signed_up") public Boolean hasSignedUp; //서비스에 연결 완료된 시각. UTC @JsonProperty("connected_at") public Date connectedAt; //카카오싱크 간편가입을 통해 로그인한 시각. UTC @JsonProperty("synched_at") public Date synchedAt; //사용자 프로퍼티 @JsonProperty("properties") public HashMap<String, String> properties; //카카오 계정 정보 @JsonProperty("kakao_account") public KakaoAccount kakaoAccount; //uuid 등 추가 정보 @JsonProperty("for_partner") public Partner partner; @Getter @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class KakaoAccount { //프로필 정보 제공 동의 여부 @JsonProperty("profile_needs_agreement") public Boolean isProfileAgree; //닉네임 제공 동의 여부 @JsonProperty("profile_nickname_needs_agreement") public Boolean isNickNameAgree; //프로필 사진 제공 동의 여부 @JsonProperty("profile_image_needs_agreement") public Boolean isProfileImageAgree; //사용자 프로필 정보 @JsonProperty("profile") public Profile profile; //이름 제공 동의 여부 @JsonProperty("name_needs_agreement") public Boolean isNameAgree; //카카오계정 이름 @JsonProperty("name") public String name; //이메일 제공 동의 여부 @JsonProperty("email_needs_agreement") public Boolean isEmailAgree; //이메일이 유효 여부 // true : 유효한 이메일, false : 이메일이 다른 카카오 계정에 사용돼 만료 @JsonProperty("is_email_valid") public Boolean isEmailValid; //이메일이 인증 여부 //true : 인증된 이메일, false : 인증되지 않은 이메일 @JsonProperty("is_email_verified") public Boolean isEmailVerified; //카카오계정 대표 이메일 @JsonProperty("email") public String email; //연령대 제공 동의 여부 @JsonProperty("age_range_needs_agreement") public Boolean isAgeAgree; //연령대 //참고 https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info @JsonProperty("age_range") public String ageRange; //출생 연도 제공 동의 여부 @JsonProperty("birthyear_needs_agreement") public Boolean isBirthYearAgree; //출생 연도 (YYYY 형식) @JsonProperty("birthyear") public String birthYear; //생일 제공 동의 여부 @JsonProperty("birthday_needs_agreement") public Boolean isBirthDayAgree; //생일 (MMDD 형식) @JsonProperty("birthday") public String birthDay; //생일 타입 // SOLAR(양력) 혹은 LUNAR(음력) @JsonProperty("birthday_type") public String birthDayType; //성별 제공 동의 여부 @JsonProperty("gender_needs_agreement") public Boolean isGenderAgree; //성별 @JsonProperty("gender") public String gender; //전화번호 제공 동의 여부 @JsonProperty("phone_number_needs_agreement") public Boolean isPhoneNumberAgree; //전화번호 //국내 번호인 경우 +82 00-0000-0000 형식 @JsonProperty("phone_number") public String phoneNumber; //CI 동의 여부 @JsonProperty("ci_needs_agreement") public Boolean isCIAgree; //CI, 연계 정보 @JsonProperty("ci") public String ci; //CI 발급 시각, UTC @JsonProperty("ci_authenticated_at") public Date ciCreatedAt; @Getter @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class Profile { //닉네임 @JsonProperty("nickname") public String nickName; //프로필 미리보기 이미지 URL @JsonProperty("thumbnail_image_url") public String thumbnailImageUrl; //프로필 사진 URL @JsonProperty("profile_image_url") public String profileImageUrl; //프로필 사진 URL 기본 프로필인지 여부 //true : 기본 프로필, false : 사용자 등록 @JsonProperty("is_default_image") public String isDefaultImage; //닉네임이 기본 닉네임인지 여부 //true : 기본 닉네임, false : 사용자 등록 @JsonProperty("is_default_nickname") public Boolean isDefaultNickName; } } @Getter @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class Partner { //고유 ID @JsonProperty("uuid") public String uuid; }}

entity 패키지

User

package com.example.sumda.entity;import jakarta.persistence.*;import lombok.AllArgsConstructor;import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter;@Entity@Table(name = "user")@Getter@Setter@NoArgsConstructor@AllArgsConstructorpublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long kakaoId; private String nickname; private String profileImageUrl;}

repository 패키지

UserRepository

package com.example.sumda.repository;import org.springframework.data.jpa.repository.JpaRepository;import com.example.sumda.entity.User;import org.springframework.stereotype.Repository;@Repositorypublic interface UserRepository extends JpaRepository<User, Long>{}

service 패키지

KakaoService

package com.example.sumda.service;import io.netty.handler.codec.http.HttpHeaderValues;import jakarta.transaction.Transactional;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatusCode;import org.springframework.stereotype.Service;import com.example.sumda.dto.KakaoTokenResponseDto;import com.example.sumda.dto.KakaoUserInfoResponseDto;import com.example.sumda.entity.User;import com.example.sumda.repository.UserRepository;import org.springframework.web.reactive.function.client.WebClient;import reactor.core.publisher.Mono;@Slf4j@Servicepublic class KakaoService { private final String clientId; private final String KAUTH_TOKEN_URL_HOST; private final String KAUTH_USER_URL_HOST; private final UserRepository userRepository; // 생성자 주입 방식으로 변경 public KakaoService(@Value("${spring.security.oauth2.client.registration.kakao.client_id}") String clientId, UserRepository userRepository) { this.clientId = clientId; this.userRepository = userRepository; this.KAUTH_TOKEN_URL_HOST = "https://kauth.kakao.com"; this.KAUTH_USER_URL_HOST = "https://kapi.kakao.com"; } public String getAccessTokenFromKakao(String code) { KakaoTokenResponseDto kakaoTokenResponseDto = WebClient.create(KAUTH_TOKEN_URL_HOST).post() .uri(uriBuilder -> uriBuilder .scheme("https") .path("/oauth/token") .queryParam("grant_type", "authorization_code") .queryParam("client_id", clientId) .queryParam("code", code) .build(true)) .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) .bodyToMono(KakaoTokenResponseDto.class) .block(); log.info(" [Kakao Service] Access Token ------> {}", kakaoTokenResponseDto.getAccessToken()); log.info(" [Kakao Service] Refresh Token ------> {}", kakaoTokenResponseDto.getRefreshToken()); log.info(" [Kakao Service] Id Token ------> {}", kakaoTokenResponseDto.getIdToken()); log.info(" [Kakao Service] Scope ------> {}", kakaoTokenResponseDto.getScope()); return kakaoTokenResponseDto.getAccessToken(); } @Transactional public KakaoUserInfoResponseDto getUserInfo(String accessToken) { KakaoUserInfoResponseDto userInfo = WebClient.create(KAUTH_USER_URL_HOST) .get() .uri(uriBuilder -> uriBuilder .scheme("https") .path("/v2/user/me") .build(true)) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) .bodyToMono(KakaoUserInfoResponseDto.class) .block(); log.info("[ Kakao Service ] Auth ID ---> {} ", userInfo.getId()); log.info("[ Kakao Service ] NickName ---> {} ", userInfo.getKakaoAccount().getProfile().getNickName()); log.info("[ Kakao Service ] ProfileImageUrl ---> {} ", userInfo.getKakaoAccount().getProfile().getProfileImageUrl()); User user = new User(); user.setKakaoId(userInfo.getId()); user.setNickname(userInfo.getKakaoAccount().getProfile().getNickName()); user.setProfileImageUrl(userInfo.getKakaoAccount().getProfile().getProfileImageUrl()); userRepository.save(user); return userInfo; }}

카카오 로그인 구현한 화면

공감이 글에 공감한 블로거 열고 닫기

댓글쓰기 이 글에 댓글 단 블로거 열고 닫기

인쇄

[프로젝트] 카카오 로그인 OAuth2 구현 (2024)
Top Articles
Easy Marinara Sauce Recipe with Extra Veggies
10 Tried-and-True Vegan Dinner Recipes that I Make Over and Over Again
Funny Roblox Id Codes 2023
Golden Abyss - Chapter 5 - Lunar_Angel
Www.paystubportal.com/7-11 Login
Joi Databas
DPhil Research - List of thesis titles
Shs Games 1V1 Lol
Evil Dead Rise Showtimes Near Massena Movieplex
Steamy Afternoon With Handsome Fernando
Which aspects are important in sales |#1 Prospection
Detroit Lions 50 50
18443168434
Newgate Honda
Zürich Stadion Letzigrund detailed interactive seating plan with seat & row numbers | Sitzplan Saalplan with Sitzplatz & Reihen Nummerierung
Grace Caroline Deepfake
978-0137606801
Nwi Arrests Lake County
Immortal Ink Waxahachie
Craigslist Free Stuff Santa Cruz
Mflwer
Spergo Net Worth 2022
Costco Gas Foster City
Obsidian Guard's Cutlass
Marvon McCray Update: Did He Pass Away Or Is He Still Alive?
Mccain Agportal
Amih Stocktwits
Fort Mccoy Fire Map
Uta Kinesiology Advising
Kcwi Tv Schedule
What Time Does Walmart Auto Center Open
Nesb Routing Number
Olivia Maeday
Random Bibleizer
10 Best Places to Go and Things to Know for a Trip to the Hickory M...
Black Lion Backpack And Glider Voucher
Gopher Carts Pensacola Beach
Duke University Transcript Request
Lincoln Financial Field, section 110, row 4, home of Philadelphia Eagles, Temple Owls, page 1
Jambus - Definition, Beispiele, Merkmale, Wirkung
Ark Unlock All Skins Command
Craigslist Red Wing Mn
D3 Boards
Jail View Sumter
Nancy Pazelt Obituary
Birmingham City Schools Clever Login
Thotsbook Com
Funkin' on the Heights
Vci Classified Paducah
Www Pig11 Net
Ty Glass Sentenced
Latest Posts
Article information

Author: Nathanial Hackett

Last Updated:

Views: 5707

Rating: 4.1 / 5 (72 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Nathanial Hackett

Birthday: 1997-10-09

Address: Apt. 935 264 Abshire Canyon, South Nerissachester, NM 01800

Phone: +9752624861224

Job: Forward Technology Assistant

Hobby: Listening to music, Shopping, Vacation, Baton twirling, Flower arranging, Blacksmithing, Do it yourself

Introduction: My name is Nathanial Hackett, I am a lovely, curious, smiling, lively, thoughtful, courageous, lively person who loves writing and wants to share my knowledge and understanding with you.