๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๋ฐฑ์—”๋“œ/spring&springboot

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„_Spring Boot ์‹ฌํ™”์ฃผ์ฐจ] ์†Œ์…œ ๋กœ๊ทธ์ธ(OAuth), ์นด์นด์˜ค ๋กœ๊ทธ์ธ

by alswlfl 2026. 7. 1.

์†Œ์…œ ๋กœ๊ทธ์ธ

์†Œ์…œ ๋กœ๊ทธ์ธ ํƒ„์ƒ ๋ฐฐ๊ฒฝ

  • ๋ชจ๋“  ์›น ์‚ฌ์ดํŠธ์—์„œ ํšŒ์›๊ฐ€์ž… ๊ณผ์ •์„ ๊ฑฐ์น˜๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ถ€๋‹ด์ด ๋จ
  • ๋งค๋ฒˆ ๋ฒˆ๊ฑฐ๋กœ์šด ํšŒ์›๊ฐ€์ž… ๊ณผ์ •์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•  ๋ฟ ์•„๋‹ˆ๋ผ, ์›น ์‚ฌ์ดํŠธ๋งˆ๋‹ค ๋‹ค๋ฅธ ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ธฐ์–ตํ•ด์•ผํ•จ
  • ๋˜ํ•œ, ์›น ์‚ฌ์ดํŠธ๋ฅผ ์šด์˜ํ•˜๋Š” ์ธก์—์„œ๋„ ํšŒ์›๋“ค์˜ ๊ฐœ์ธ์ •๋ณด๋ฅผ ์ง€์ผœ์•ผํ•˜๋Š” ์—ญํ• ์ด ๋ถ€๋‹ด๋จ
  • ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด OAuth๋ฅผ ์‚ฌ์šฉํ•œ ์†Œ์…œ ๋กœ๊ทธ์ธ์ด ๋“ฑ์žฅํ•จ

์†Œ์…œ ๋กœ๊ทธ์ธ ์˜ˆ์‹œ(์™“์ฑ )

OAuth

OAuth๋Š” ์ธํ„ฐ๋„ท ์‚ฌ์šฉ์ž๋“ค์ด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ์›น์‚ฌ์ดํŠธ ์ƒ์˜ ์ž์‹ ๋“ค์˜ ์ •๋ณด์— ๋Œ€ํ•ด ์›น์‚ฌ์ดํŠธ๋‚˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ ‘๊ทผ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ณตํ†ต์ ์ธ ์ˆ˜๋‹จ์œผ๋กœ์„œ ์‚ฌ์šฉ๋˜๋Š” ์ ‘๊ทผ ์œ„์ž„์„ ์œ„ํ•œ ๊ฐœ๋ฐฉํ–ฅ ํ‘œ์ค€

  • ์‚ฌ์šฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—๊ฒŒ ๋ชจ๋“  ๊ถŒํ•œ์„ ๋„˜๊ธฐ์ง€ ์•Š๊ณ  ์‚ฌ์šฉ์ž ๋Œ€์‹  ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” HTTP ๊ธฐ๋ฐ˜์˜ ๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ
  • OAuth๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์„œ๋น„์Šค ์ œ๊ณต์ž๋Š” ๋Œ€ํ‘œ์ ์œผ๋กœ ๊ตฌ๊ธ€, ํŽ˜์ด์Šค๋ถ ๋“ฑ์ด ์žˆ์Œ. ๊ตญ๋‚ด์—๋Š” ๋Œ€ํ‘œ์ ์œผ๋กœ ์นด์นด์˜ค, ๋„ค์ด๋ฒ„ ์กด์žฌ

์นด์นด์˜ค ๋กœ๊ทธ์ธ

์นด์นด์˜ค ๋กœ๊ทธ์ธ ๊ณผ์ •

์นด์นด์˜ค ๋กœ๊ทธ์ธ ์‚ฌ์šฉ ์Šน์ธ ๋ฐ›๊ธฐ

1. ์นด์นด์˜ค ๋””๋ฒจ๋กœํผ์Šค ์ ‘์† ํ›„ ๋กœ๊ทธ์ธ

2. ์ƒ๋‹จ์— ์•ฑ ํด๋ฆญ > ์šฐ์ธก์— ์•ฑ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ

3. ์•ฑ ์ƒ์„ฑ ๋ชจ๋‹ฌ ์ฐฝ ๋‚ด์— ์ •๋ณด ์ž…๋ ฅ ํ›„ ์ €์žฅ ํด๋ฆญ

4. JavaScript SDK ๋„๋ฉ”์ธ๊ณผ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ์ž…๋ ฅ

  • ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋Š” ์ธ๊ฐ€ ํ† ํฐ์„ ๋ฐ›์„ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI์ž„

5. ๋ฉ”๋‰ด > ์ œํ’ˆ ์„ค๋ช… > ์นด์นด์˜ค ๋กœ๊ทธ์ธ > ์ผ๋ฐ˜ ํด๋ฆญ ํ›„, ์‚ฌ์šฉ ์„ค์ • ON์œผ๋กœ ๋ณ€๊ฒฝ

6. ๋ฉ”๋‰ด > ์•ฑ > ์•ฑ ์„ค์ • > ์•ฑ > ์ผ๋ฐ˜ > ๋น„์ฆˆ๋‹ˆ์Šค ์ •๋ณด์˜ ๊ฐœ์ธ ๊ฐœ๋ฐœ์ž ๋น„์ฆˆ ์•ฑ์˜ '์นด์นด์˜ค๋น„์ฆˆ๋‹ˆ์Šค ํ†ตํ•ฉ ์„œ๋น„์Šค ์•ฝ๊ด€ ๋™์˜' ํด๋ฆญ

  • ์ผ๋ฐ˜ ์•ฑ์ด ์•„๋‹ˆ๋ผ ๋น„์ฆˆ ์•ฑ์œผ๋กœ ์ธ์ •์ด ๋˜์–ด์•ผ๋งŒ ์นด์นด์˜ค ์ด๋ฉ”์ผ ๊ณ„์ • ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Œ

7. ๋™์˜ํ•ญ๋ชฉ ์„ค์ •


์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ

login.html ๋‚ด ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ์ถ”๊ฐ€

<button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id={JAVASCRIPT_KEY}&redirect_uri=http://localhost:8080/api/user/kakao/callback&response_type=code'">
	์นด์นด์˜ค๋กœ ๋กœ๊ทธ์ธํ•˜๊ธฐ
</button>
  • JAVASCRIPT_KEY ํ‚ค ์ถ”๊ฐ€

JavaScript ํ‚ค

  • ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ง„ํ–‰์ด ๋˜๋ฉด, Redirect URI(http://localhost:8080/api/user/kakao/callback)๋กœ ํ•ด๋‹น ์š”์ฒญ(+์ธ๊ฐ€ ์ฝ”๋“œ)์ด ๋ฆฌํ„ด๋จ

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์š”์ฒญ์„ ๋ฐ›์„ ๋กœ์ง

Controller ์ถ”๊ฐ€(UserController)

@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/api")
public class UserController {

    private final UserService userService;
    private final FolderService folderService;
    private final KakaoService kakaoService;
    
    ...

    @GetMapping("/user/kakao/callback")
    public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
        String token = kakaoService.kakaoLogin(code);

        // ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„ฑ๊ณตํ•˜๋ฉด template๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        return "redirect:/";
    }
}

 

๋กœ์ง ์ฒ˜๋ฆฌํ•  Service ์ถ”๊ฐ€(KakaoService)

 

์„œ๋น„์Šค์—์„œ๋Š” ์‹ค์งˆ์ ์œผ๋กœ ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›๊ณ , ๊ทธ ํ† ํฐ์„ ์‚ฌ์šฉํ•ด์„œ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ณ  ์ตœ์ข…์ ์œผ๋กœ ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰๊ณผ JWT ๋ฐ˜ํ™˜

  • Kakao์—์„œ ๋™์˜ํ•ญ๋ชฉ ์„ค์ •ํ•œ ์ •๋ณด์— ๋Œ€ํ•œ DTO ์ถ”๊ฐ€
package com.sparta.myselectshop.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class KakaoUserInfoDto {
    private Long id;
    private String nickname;
    private String email;

    public KakaoUserInfoDto(Long id, String nickname, String email) {
        this.id = id;
        this.nickname = nickname;
        this.email = email;
    }
}
  • RestTemplate Bean ์ˆ˜๋™ ๋“ฑ๋ก์„ ์œ„ํ•œ RestTemplateConfig ์ถ”๊ฐ€
package com.sparta.myselectshop.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Configuration
public class RestTemplateConfig  {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                // RestTemplate ์œผ๋กœ ์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹œ ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚˜๋„ ์‘๋‹ต์ด ์—†์„ ๋•Œ
                // ๋ฌดํ•œ ๋Œ€๊ธฐ ์ƒํƒœ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๊ฐ•์ œ ์ข…๋ฃŒ ์„ค์ •
                .setConnectTimeout(Duration.ofMillis(5000)) // 5์ดˆ
                .setReadTimeout(Duration.ofMillis(5000)) // 5์ดˆ
                .build();
    }
}
  • ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ์•ก์„ธ์Šค ํ† ํฐ ์š”์ฒญํ•  getToken ๋ฉ”์„œ๋“œ ์ƒ์„ฑ
private String getToken(String code) throws JsonProcessingException {
    // ์š”์ฒญ URL ๋งŒ๋“ค๊ธฐ
    URI uri = UriComponentsBuilder
            .fromUriString("https://kauth.kakao.com")
            .path("/oauth/token")
            .encode()
            .build()
            .toUri();

    // HTTP Header ์ƒ์„ฑ
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    // HTTP Body ์ƒ์„ฑ
    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
    body.add("grant_type", "authorization_code");
    body.add("client_id", "๋ณธ์ธ์˜ REST APIํ‚ค"); // REST API ํ‚ค ์ถ”๊ฐ€
    body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback");
    body.add("code", code);
    
    // Client Secret ํ‚ค ์ถ”๊ฐ€
    body.add("client_secret", "๋ณธ์ธ์˜ Client Secret ์ฝ”๋“œ");

    RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
            .post(uri)
            .headers(headers)
            .body(body);

    // HTTP ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
    ResponseEntity<String> response = restTemplate.exchange(
            requestEntity,
            String.class
    );

    // HTTP ์‘๋‹ต (JSON) -> ์•ก์„ธ์Šค ํ† ํฐ ํŒŒ์‹ฑ
    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    return jsonNode.get("access_token").asText();
}
  • ํ† ํฐ์„ ์‚ฌ์šฉํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
    // ์š”์ฒญ URL ๋งŒ๋“ค๊ธฐ
    URI uri = UriComponentsBuilder
            .fromUriString("https://kapi.kakao.com")
            .path("/v2/user/me")
            .encode()
            .build()
            .toUri();

    // HTTP Header ์ƒ์„ฑ
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Bearer " + accessToken);
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
            .post(uri)
            .headers(headers)
            .body(new LinkedMultiValueMap<>());

    // HTTP ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
    ResponseEntity<String> response = restTemplate.exchange(
            requestEntity,
            String.class
    );

    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    Long id = jsonNode.get("id").asLong();
    String nickname = jsonNode.get("properties")
            .get("nickname").asText();
    String email = jsonNode.get("kakao_account")
            .get("email").asText();

    log.info("์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด: " + id + ", " + nickname + ", " + email);
    return new KakaoUserInfoDto(id, nickname, email);
}
  • Service ๋‚ด kakaoLogin ๋ฉ”์„œ๋“œ
package com.sparta.myselectshop.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.myselectshop.dto.KakaoUserInfoDto;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final RestTemplate restTemplate; // RestTemplate์€ RestTemplateBuilder๋ฅผ ํ†ตํ•ด์„œ ์ƒ์„ฑ(์ˆ˜๋™ ๋“ฑ๋ก)
    private final JwtUtil jwtUtil;

    public String kakaoLogin(String code) throws JsonProcessingException {
        // 1. "์ธ๊ฐ€ ์ฝ”๋“œ"๋กœ "์•ก์„ธ์Šค ํ† ํฐ" ์š”์ฒญ
        String accessToken = getToken(code); // ์นด์นด์˜ค ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ

        // 2. ํ† ํฐ์œผ๋กœ ์นด์นด์˜ค API ํ˜ธ์ถœ : "์•ก์„ธ์Šค ํ† ํฐ"์œผ๋กœ "์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด" ๊ฐ€์ ธ์˜ค๊ธฐ
        KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);

        return null;
    }

    private String getToken(String code) throws JsonProcessingException {
        ...
    }

    private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        ...
    }

}

์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ํšŒ์›๊ฐ€์ž… ๊ตฌํ˜„

ํ˜„์žฌ ํšŒ์›(User) ํ…Œ์ด๋ธ”

์ปฌ๋Ÿผ๋ช… ์ปฌ๋Ÿผํƒ€์ž… ์ค‘๋ณตํ—ˆ์šฉ ์„ค๋ช…
id Long X ํ…Œ์ด๋ธ” ID(PK)
username String X ํšŒ์› ID
password String O ํŒจ์Šค์›Œ๋“œ
email String X ์ด๋ฉ”์ผ ์ฃผ์†Œ
role String O ์—ญํ• 
1) ์‚ฌ์šฉ์ž: USER
2) ๊ด€๋ฆฌ์ž: ADMIN

 

์นด์นด์˜ค๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด

  • kakaoId
  • nickname
  • email

์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด JSON ์˜ˆ์‹œ

ํ…Œ์ด๋ธ” ์„ค๊ณ„ ์˜ต์…˜

1. ์นด์นด์˜ค User๋ฅผ ์œ„ํ•œ ํ…Œ์ด๋ธ”(ex: KakaoUser) ํ•˜๋‚˜ ๋” ๋งŒ๋“ค๊ธฐ

  • ์žฅ์ : ๊ฒฐํ•ฉ๋„๊ฐ€ ๋‚ฎ์•„์ง
    • ์„ฑ๊ฒฉ์ด ๋‹ค๋ฅธ ์œ ์ € ๋ณ„๋กœ ๋ถ„๋ฆฌ → ์ฐจํ›„ ๊ฐ ํ…Œ์ด๋ธ”์˜ ๋ณ€ํ™”์— ์„œ๋กœ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ
    • ex) ์นด์นด์˜ค ์‚ฌ์šฉ์ž๋“ค๋งŒ profile_image ์ปฌ๋Ÿผ ์ถ”๊ฐ€ํ•ด์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ๋‹จ์ : ๊ตฌํ˜„ ๋‚œ์ด๋„ ์˜ฌ๋ผ๊ฐ
    • ex) ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก ์‹œ, ํšŒ์›๋ณ„๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•ด์•ผ ํ•จ
      - ์ผ๋ฐ˜ ํšŒ์›: User-Product
      - ์นด์นด์˜ค ํšŒ์›: KakaoUser-Product

2. ๊ธฐ์กด ํšŒ์›(User) ํ…Œ์ด๋ธ”์— ์นด์นด์˜ค User ์ถ”๊ฐ€

  • ์žฅ์ : ๊ตฌํ˜„์ด ๋‹จ์ˆœํ•ด์ง
  • ๋‹จ์ : ๊ฒฐํ•ฉ๋„๊ฐ€ ๋†’์•„์ง
    • ํผ ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ username, password๋ฅผ ์ž…๋ ฅํ•ด์„œ ๋กœ๊ทธ์ธ์„ ํ•  ๊ฒฝ์šฐ, ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ถ€๋ถ„์ด ๋งŽ์•„์ง

ํšŒ์› ํ…Œ์ด๋ธ”์— ์นด์นด์˜ค User ์ถ”๊ฐ€

์ปฌ๋Ÿผ๋ช… ์ปฌ๋Ÿผํƒ€์ž… ์ค‘๋ณตํ—ˆ์šฉ ์„ค๋ช… ์นด์นด์˜ค ์‚ฌ์šฉ์ž
id Long X ํ…Œ์ด๋ธ” ID(PK) ํ…Œ์ด๋ธ” ID(PK)
username String X ํšŒ์› ID nickname
password String O ํŒจ์Šค์›Œ๋“œ UUID(๋žœ๋ค์œผ๋กœ ์ƒ์„ฑํ•œ ๋ฌธ์ž์—ด)
email String X ์ด๋ฉ”์ผ ์ฃผ์†Œ email
role String O ์—ญํ• 
1) ์‚ฌ์šฉ์ž: USER
2) ๊ด€๋ฆฌ์ž: ADMIN
"USER"๋กœ ๊ณ ์ •
kakaoId String(Nullable) X ์นด์นด์˜ค ๋กœ๊ทธ์ธ ID kakaoId

์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ํšŒ์›๊ฐ€์ž… ๊ตฌํ˜„

ํ๋ฆ„

ํšŒ์›๊ฐ€์ž… ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€(ํ•„์š” ์‹œ์— ์ง„ํ–‰)

  • ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ ์นด์นด์˜ค ์„œ๋น„์Šค์—์„œ ๊ฐ€์ ธ์˜จ ์•ก์„ธ์Šค ํ† ํฐ์œผ๋กœ ์นด์นด์˜ค ์œ ์ € ์ •๋ณด ๊ฐ€์ ธ์˜ด
public String kakaoLogin(String code) throws JsonProcessingException {
    // 1. "์ธ๊ฐ€ ์ฝ”๋“œ"๋กœ "์•ก์„ธ์Šค ํ† ํฐ" ์š”์ฒญ
    String accessToken = getToken(code); // ์นด์นด์˜ค ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ

    // 2. ํ† ํฐ์œผ๋กœ ์นด์นด์˜ค API ํ˜ธ์ถœ : "์•ก์„ธ์Šค ํ† ํฐ"์œผ๋กœ "์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด" ๊ฐ€์ ธ์˜ค๊ธฐ
    KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);

    // 3. ํ•„์š”์‹œ์— ํšŒ์›๊ฐ€์ž…
    User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);

    // 4. JWT ํ† ํฐ ๋ฐ˜ํ™˜
    String createToken = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());

    return createToken;
}

private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
    // DB ์— ์ค‘๋ณต๋œ Kakao Id ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
    Long kakaoId = kakaoUserInfo.getId();
    User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);

    if (kakaoUser == null) {
        // ์นด์นด์˜ค ์‚ฌ์šฉ์ž email๊ณผ ๋™์ผํ•œ email ๊ฐ€์ง„ ํšŒ์›์ด ์žˆ๋Š”์ง€ ํ™•์ธ
        // ex) aaa.kakao.com์œผ๋กœ ์ง์ ‘ ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰ํ•œ ํ›„, ์ถ”ํ›„์— ์นด์นด์˜ค ์†Œ์…œ ๋กœ๊ทธ์ธ์œผ๋กœ ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ
        String kakaoEmail = kakaoUserInfo.getEmail();
        User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
        if (sameEmailUser != null) {
            kakaoUser = sameEmailUser;
            // ๊ธฐ์กด ํšŒ์›์ •๋ณด์— ์นด์นด์˜ค Id ์ถ”๊ฐ€
            kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
        } else {
            // ์‹ ๊ทœ ํšŒ์›๊ฐ€์ž…
            // password: random UUID
            String password = UUID.randomUUID().toString();
            String encodedPassword = passwordEncoder.encode(password);

            // email: kakao email
            String email = kakaoUserInfo.getEmail();

            kakaoUser = new User(kakaoUserInfo.getNickname(), encodedPassword, email, UserRoleEnum.USER, kakaoId);
        }

        userRepository.save(kakaoUser);
    }
    return kakaoUser;
}
  • User ์—”ํ‹ฐํ‹ฐ์— ์นด์นด์˜ค User ์ถ”๊ฐ€
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    private Long kakaoId;

    public User(String username, String password, String email, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
    }

    public User(String username, String password, String email, UserRoleEnum role, Long kakaoId) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
        this.kakaoId =kakaoId;
    }

    public User kakaoIdUpdate(Long kakaoId) {
        this.kakaoId = kakaoId;
        return this;
    }
}
  • ํ† ํฐ์„ ์ฟ ํ‚ค์— ์ง์ ‘ ๋„ฃ์–ด์ฃผ๊ธฐ
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/api")
public class UserController {

    private final UserService userService;
    private final FolderService folderService;
    private final KakaoService kakaoService;

    @GetMapping("/user/kakao/callback")
    public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
        String token = kakaoService.kakaoLogin(code);

        Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7)); // ์ˆœ์ˆ˜ํ•œ ์ฟ ํ‚ค(Bearer ์—†์• ๊ธฐ)
        cookie.setPath("/");
        response.addCookie(cookie);

        // ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„ฑ๊ณตํ•˜๋ฉด template๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        return "redirect:/";
    }
}