Skip to content

Commit e434404

Browse files
committed
feat: 카카오 로그인 구현 #23
1 parent ef59aa7 commit e434404

File tree

5 files changed

+160
-7
lines changed

5 files changed

+160
-7
lines changed

src/main/java/org/dfbf/soundlink/domain/user/controller/AuthController.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,21 @@
44
import io.swagger.v3.oas.annotations.tags.Tag;
55
import jakarta.servlet.http.HttpServletRequest;
66
import jakarta.servlet.http.HttpServletResponse;
7-
import lombok.AllArgsConstructor;
7+
import lombok.RequiredArgsConstructor;
88
import org.dfbf.soundlink.domain.user.dto.request.LoginReqDto;
9+
import org.dfbf.soundlink.domain.user.service.KakaoAuthService;
910
import org.dfbf.soundlink.domain.user.service.UserService;
1011
import org.dfbf.soundlink.global.exception.ResponseResult;
11-
import org.springframework.web.bind.annotation.PostMapping;
12-
import org.springframework.web.bind.annotation.RequestBody;
13-
import org.springframework.web.bind.annotation.RequestMapping;
14-
import org.springframework.web.bind.annotation.RestController;
12+
import org.springframework.web.bind.annotation.*;
1513

1614
@RestController
17-
@AllArgsConstructor
15+
@RequiredArgsConstructor
1816
@Tag(name = "Auth API", description = "인증 관련 API")
1917
@RequestMapping("/api/auth")
2018
public class AuthController {
2119

2220
private final UserService userService;
21+
private final KakaoAuthService kakaoAuthService;
2322

2423
@PostMapping("/login")
2524
@Operation(summary = "로그인", description = "로그인 API")
@@ -38,4 +37,11 @@ public ResponseResult logout(HttpServletResponse response, HttpServletRequest re
3837
public ResponseResult reissueToken(HttpServletRequest request, HttpServletResponse response){
3938
return userService.reissueToken(request, response);
4039
}
40+
41+
// 카카오 로그인 (인가 코드 받아서 회원가입 또는 로그인 진행)
42+
@Operation(summary = "카카오 로그인", description = "카카오 로그인 후 JWT 발급")
43+
@GetMapping("/login/kakao")
44+
public ResponseResult kakaoCallback(@RequestParam String code) {
45+
return kakaoAuthService.kakaoLogin(code);
46+
}
4147
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.dfbf.soundlink.domain.user.dto.response;
2+
3+
public record KakaoTokenResponseDTO(
4+
String token_type,
5+
String access_token,
6+
String refresh_token,
7+
int expires_in,
8+
int refresh_token_expires_in,
9+
String scope
10+
) {
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.dfbf.soundlink.domain.user.dto.response;
2+
3+
public record KakaoUserDTO(
4+
String id, // id는 Long 타입이지만, 안전성을 위해 String으로 처리
5+
KakaoAccount kakao_account
6+
) {
7+
public record KakaoAccount(
8+
String email,
9+
String nickname
10+
) {
11+
}
12+
}

src/main/java/org/dfbf/soundlink/domain/user/repository/UserRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import org.dfbf.soundlink.domain.user.entity.User;
44
import org.dfbf.soundlink.domain.user.repository.dsl.UserRepositoryCustom;
5-
import org.springframework.cache.annotation.CachePut;
65
import org.springframework.data.jpa.repository.JpaRepository;
76

87
import java.util.Optional;
@@ -15,4 +14,6 @@ public interface UserRepository extends JpaRepository<User, Long>, UserRepositor
1514
boolean existsByLoginId(String loginId);
1615

1716
Optional<User> findByLoginId(String loginId);
17+
18+
Optional<User> findBySocialId(Long socialId);
1819
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package org.dfbf.soundlink.domain.user.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.dfbf.soundlink.domain.user.dto.response.KakaoTokenResponseDTO;
5+
import org.dfbf.soundlink.domain.user.dto.response.KakaoUserDTO;
6+
import org.dfbf.soundlink.domain.user.entity.User;
7+
import org.dfbf.soundlink.domain.user.repository.UserRepository;
8+
import org.dfbf.soundlink.global.auth.JwtProvider;
9+
import org.dfbf.soundlink.global.auth.client.KakaoAuthClient;
10+
import org.dfbf.soundlink.global.auth.client.KakaoUserClient;
11+
import org.dfbf.soundlink.global.comm.enums.SocialType;
12+
import org.dfbf.soundlink.global.exception.ResponseResult;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
15+
import org.springframework.stereotype.Service;
16+
import org.springframework.util.LinkedMultiValueMap;
17+
import org.springframework.util.MultiValueMap;
18+
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
public class KakaoAuthService {
25+
26+
private final KakaoAuthClient kakaoAuthClient;
27+
private final KakaoUserClient kakaoUserClient;
28+
private final UserRepository userRepository;
29+
private final JwtProvider jwtProvider;
30+
private final BCryptPasswordEncoder passwordEncoder;
31+
private final TokenService tokenService;
32+
33+
@Value("${kakao.client-id}")
34+
private String clientId;
35+
36+
@Value("${kakao.redirect-uri}")
37+
private String redirectUri;
38+
39+
/**
40+
* 카카오 로그인 및 JWT 발급
41+
*/
42+
public ResponseResult kakaoLogin(String code) {
43+
// 요청 파라미터 설정 (JSONObject 대신 하나의 Key와 하나 이상의 value로 이루어진 리스트를 쌍으로 받기 위해 LinkedMultiValueMap 사용)
44+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
45+
params.add("grant_type", "authorization_code");
46+
params.add("client_id", clientId);
47+
params.add("redirect_uri", redirectUri);
48+
params.add("code", code);
49+
50+
// 카카오 토큰 정보 요청
51+
KakaoTokenResponseDTO tokenResponse = kakaoAuthClient.getAccessToken(params);
52+
String accessToken = tokenResponse.access_token();
53+
54+
// 카카오 사용자 정보 요청
55+
KakaoUserDTO kakaoUser = kakaoUserClient.getUserInfo("Bearer " + accessToken);
56+
String kakaoNickname = kakaoUser.kakao_account().nickname(); // 닉네임
57+
58+
// DB에서 사용자 조회
59+
Optional<User> existingUser = userRepository.findBySocialId(Long.valueOf(kakaoUser.id()));
60+
61+
if (existingUser.isPresent()) {
62+
// 기존 사용자가 존재 시, 로그인 처리
63+
return generateTokenResponse(existingUser.get());
64+
} else {
65+
// 신규 가입은 닉네임 중복 확인 필요
66+
if (userRepository.existsByNickname(kakaoNickname)) {
67+
// 기존 닉네임이 중복 시, 새로운 임의의 닉네임 생성
68+
kakaoNickname = generateUniqueNickname(kakaoNickname);
69+
}
70+
// 회원가입
71+
User newUser = registerNewKakaoUser(kakaoUser, kakaoNickname);
72+
return generateTokenResponse(newUser);
73+
}
74+
}
75+
76+
/**
77+
* JWT 토큰 생성
78+
*/
79+
private ResponseResult generateTokenResponse(User user) {
80+
String jwtAccessToken = jwtProvider.createAccessToken(user.getUserId());
81+
String jwtRefreshToken = jwtProvider.createRefreshToken(user.getUserId());
82+
83+
tokenService.updateRefreshToken(user.getUserId(), jwtRefreshToken);
84+
85+
Map<String, String> authResponse = Map.of(
86+
"accessToken", jwtAccessToken,
87+
"refreshToken", jwtRefreshToken
88+
);
89+
90+
return new ResponseResult(authResponse);
91+
}
92+
93+
/**
94+
* 새로운 카카오 사용자 회원가입
95+
*/
96+
private User registerNewKakaoUser(KakaoUserDTO kakaoUser, String nickname) {
97+
User newUser = User.builder()
98+
.nickName(nickname)
99+
.socialId(Long.valueOf(kakaoUser.id()))
100+
.socialType(SocialType.KAKAO)
101+
.loginId("kakao_" + kakaoUser.id()) // 카카오 ID 기반 로그인 ID 생성
102+
.password(passwordEncoder.encode("kakao_password")) // 비밀번호는 임의 값 (카카오 로그인이므로 필요 없음)
103+
.email(kakaoUser.kakao_account().email())
104+
.build();
105+
return userRepository.save(newUser);
106+
}
107+
108+
109+
/**
110+
* 닉네임이 중복될 경우 새로운 닉네임 생성
111+
*/
112+
private String generateUniqueNickname(String baseNickname) {
113+
int suffix = 1;
114+
String newNickname = baseNickname;
115+
116+
// 닉네임이 중복되지 않을 때까지 반복
117+
while (userRepository.existsByNickname(newNickname)) {
118+
newNickname = baseNickname + "_" + suffix;
119+
suffix++;
120+
}
121+
return newNickname;
122+
}
123+
}

0 commit comments

Comments
 (0)