์์ ๋ก๊ทธ์ธ
์์ ๋ก๊ทธ์ธ ํ์ ๋ฐฐ๊ฒฝ
- ๋ชจ๋ ์น ์ฌ์ดํธ์์ ํ์๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์น๋ ๊ฒ์ ์ฌ์ฉ์์๊ฒ ๋ถ๋ด์ด ๋จ
- ๋งค๋ฒ ๋ฒ๊ฑฐ๋ก์ด ํ์๊ฐ์ ๊ณผ์ ์ ์ํํด์ผ ํ ๋ฟ ์๋๋ผ, ์น ์ฌ์ดํธ๋ง๋ค ๋ค๋ฅธ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ธฐ์ตํด์ผํจ
- ๋ํ, ์น ์ฌ์ดํธ๋ฅผ ์ด์ํ๋ ์ธก์์๋ ํ์๋ค์ ๊ฐ์ธ์ ๋ณด๋ฅผ ์ง์ผ์ผํ๋ ์ญํ ์ด ๋ถ๋ด๋จ
- ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด 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 ํค ์ถ๊ฐ

- ์นด์นด์ค ๋ก๊ทธ์ธ ์งํ์ด ๋๋ฉด, 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 | ํจ์ค์๋ |
| String | X | ์ด๋ฉ์ผ ์ฃผ์ | |
| role | String | O | ์ญํ 1) ์ฌ์ฉ์: USER 2) ๊ด๋ฆฌ์: ADMIN |
์นด์นด์ค๋ก๋ถํฐ ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด
- kakaoId
- nickname

ํ ์ด๋ธ ์ค๊ณ ์ต์
1. ์นด์นด์ค User๋ฅผ ์ํ ํ ์ด๋ธ(ex: KakaoUser) ํ๋ ๋ ๋ง๋ค๊ธฐ
- ์ฅ์ : ๊ฒฐํฉ๋๊ฐ ๋ฎ์์ง
- ์ฑ๊ฒฉ์ด ๋ค๋ฅธ ์ ์ ๋ณ๋ก ๋ถ๋ฆฌ → ์ฐจํ ๊ฐ ํ ์ด๋ธ์ ๋ณํ์ ์๋ก ์ํฅ์ ์ฃผ์ง ์์
- ex) ์นด์นด์ค ์ฌ์ฉ์๋ค๋ง profile_image ์ปฌ๋ผ ์ถ๊ฐํด์ ์ฌ์ฉ ๊ฐ๋ฅ
- ๋จ์ : ๊ตฌํ ๋์ด๋ ์ฌ๋ผ๊ฐ
- ex) ๊ด์ฌ์ํ ๋ฑ๋ก ์, ํ์๋ณ๋ก ๋ค๋ฅธ ํ
์ด๋ธ์ ์ฐธ์กฐํด์ผ ํจ
- ์ผ๋ฐ ํ์: User-Product
- ์นด์นด์ค ํ์: KakaoUser-Product
- ex) ๊ด์ฌ์ํ ๋ฑ๋ก ์, ํ์๋ณ๋ก ๋ค๋ฅธ ํ
์ด๋ธ์ ์ฐธ์กฐํด์ผ ํจ
2. ๊ธฐ์กด ํ์(User) ํ ์ด๋ธ์ ์นด์นด์ค User ์ถ๊ฐ
- ์ฅ์ : ๊ตฌํ์ด ๋จ์ํด์ง
- ๋จ์ : ๊ฒฐํฉ๋๊ฐ ๋์์ง
- ํผ ๋ก๊ทธ์ธ์ ํตํด ์นด์นด์ค ๋ก๊ทธ์ธ ์ฌ์ฉ์์ username, password๋ฅผ ์ ๋ ฅํด์ ๋ก๊ทธ์ธ์ ํ ๊ฒฝ์ฐ, ๊ณ ๋ คํด์ผ ํ ๋ถ๋ถ์ด ๋ง์์ง
ํ์ ํ ์ด๋ธ์ ์นด์นด์ค User ์ถ๊ฐ
| ์ปฌ๋ผ๋ช | ์ปฌ๋ผํ์ | ์ค๋ณตํ์ฉ | ์ค๋ช | ์นด์นด์ค ์ฌ์ฉ์ |
| id | Long | X | ํ ์ด๋ธ ID(PK) | ํ ์ด๋ธ ID(PK) |
| username | String | X | ํ์ ID | nickname |
| password | String | O | ํจ์ค์๋ | UUID(๋๋ค์ผ๋ก ์์ฑํ ๋ฌธ์์ด) |
| String | X | ์ด๋ฉ์ผ ์ฃผ์ | ||
| 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:/";
}
}