์๋ ํ์ธ์! ์ง๋ ํฌ์คํ ์์๋ ์นด์นด์ค ์์ ๋ก๊ทธ์ธ ๊ตฌํ์ ์ํ ๊ธฐ๋ณธ ์ค์ ๊ณผ DTO ํด๋์ค๋ค์ ์์๋ณด์์ต๋๋ค. ์ด๋ฒ ๊ธ์์๋ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ค์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ฒ๋ฆฌํ๋ ์๋น์ค์ ์ปจํธ๋กค๋ฌ ์ฝ๋๋ฅผ ๊ตฌํํด๋ณผ๊ฒ์.
์ด ๊ธ์ ํตํด OAuth 2.0 ์ธ์ฆ ํ๋ฆ์ ์ดํดํ๊ณ , ์นด์นด์ค API์ ํต์ ํ๋ ๋ฐฉ๋ฒ, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ ๋ฑ์ ์์ธํ ์์๋ด ์๋ค! ๐
์์ ๋ก๊ทธ์ธ์ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ์๋น์ค ๊ณ์ธต์์ ์ฒ๋ฆฌํฉ๋๋ค. ์ด์ ์นด์นด์ค API์ ํต์ ํ๊ณ ์ฌ์ฉ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
@Service
@RequiredArgsConstructor
public class KakaoLoginService {
// ์์กด์ฑ ์ฃผ์
private final KakaoOAuthConfig kakaoOAuthConfig;
private final UserMapper userMapper;
private final SocialLoginMapper socialLoginMapper;
private final LogRepository logRepository;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenMapper refreshTokenMapper;
private final RestTemplate restTemplate;
// ์์ ์ ์
private static final int KAKAO_SOCIAL_CODE = 4; // ์นด์นด์ค ์์
์ฝ๋
private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
private static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(7);
private static final Duration ACCESS_TOKEN_DURATION = Duration.ofMinutes(30);
// ๋ฉ์๋๋ค์ ์๋์ ๊ตฌํ
}
๐ก Tip:
@RequiredArgsConstructor
๋final
ํ๋์ ๋ํ ์์ฑ์๋ฅผ ์๋์ผ๋ก ์์ฑํด์ฃผ๋ ๋กฌ๋ณต ์ด๋ ธํ ์ด์ ์ ๋๋ค. ์คํ๋ง์ ์์ฑ์ ๊ธฐ๋ฐ ์์กด์ฑ ์ฃผ์ ์ ๊ฐ๊ฒฐํ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
/**
* ์นด์นด์ค ์ธ์ฆ URL ๋ฐํ
* ์ฌ์ฉ์๋ฅผ ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธํ๊ธฐ ์ํ URL์ ์์ฑํฉ๋๋ค.
*/
public String getKakaoAuthUrl() {
return kakaoOAuthConfig.getAuthorizationUrl();
}
์ด ๋ฉ์๋๋ ๊ฐ๋จํ๊ฒ ์ค์ ํด๋์ค์์ ์์ฑํ ์ธ์ฆ URL์ ๋ฐํํฉ๋๋ค. ์ปจํธ๋กค๋ฌ์์ ์ด URL๋ก ์ฌ์ฉ์๋ฅผ ๋ฆฌ๋ค์ด๋ ํธํ ๊ฒ์ ๋๋ค.
์ด์ ๊ฐ์ฅ ์ค์ํ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ฉ์๋๋ฅผ ๊ตฌํํด๋ด ์๋ค.
/**
* ์นด์นด์ค ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
* ์นด์นด์ค ์ธ์ฆ ํ ๋ฐ์ ์ฝ๋๋ก ์ก์ธ์ค ํ ํฐ์ ์์ฒญํ๊ณ , ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ์ฌ ๋ก๊ทธ์ธ/ํ์๊ฐ์
์ ์ฒ๋ฆฌํฉ๋๋ค.
*/
@Transactional
public LoginResponseDto kakaoLogin(String code, HttpServletRequest request, HttpServletResponse response) {
try {
// 1. ์นด์นด์ค๋ก๋ถํฐ ์ก์ธ์ค ํ ํฐ ํ๋
KakaoTokenResponse tokenResponse = getKakaoAccessToken(code);
if (tokenResponse == null || tokenResponse.getAccess_token() == null) {
throw new RuntimeException("Failed to get access token from Kakao");
}
// 2. ์ก์ธ์ค ํ ํฐ์ผ๋ก ์นด์นด์ค API ํธ์ถํ์ฌ ์ฌ์ฉ์ ์ ๋ณด ํ๋
KakaoUserInfoResponse userInfo = getKakaoUserInfo(tokenResponse.getAccess_token());
if (userInfo == null || userInfo.getId() == null) {
throw new RuntimeException("Failed to get user info from Kakao");
}
// 3. ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ์๊ฐ์
๋๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
User user = processKakaoUser(userInfo, tokenResponse.getAccess_token());
// 4. JWT ํ ํฐ ์์ฑ
String accessToken = jwtTokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
// 5. ๋ฆฌํ๋ ์ ํ ํฐ ์์ฑ ๋ฐ ์ ์ฅ
String refreshToken = jwtTokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
saveRefreshToken(user.getUserNo(), refreshToken, response);
// 6. ๋ก๊ทธ์ธ ์ฑ๊ณต ๋ก๊ทธ ๊ธฐ๋ก
saveLog(user.getUserNo(), "KAKAO_LOGIN_SUCCESS",
"์นด์นด์ค ๋ก๊ทธ์ธ ์ฑ๊ณต: " + userInfo.getId(),
getClientIp(request), request.getHeader("User-Agent"));
// 7. ๋ก๊ทธ์ธ ์๋ต ์์ฑ
return LoginResponseDto.builder()
.userId(user.getUserNo())
.username(user.getUsername())
.token(accessToken)
.build();
} catch (Exception e) {
// ๋ก๊ทธ์ธ ์คํจ ๋ก๊ทธ ๊ธฐ๋ก
saveLog(null, "KAKAO_LOGIN_FAIL",
"์นด์นด์ค ๋ก๊ทธ์ธ ์คํจ: " + e.getMessage(),
getClientIp(request), request.getHeader("User-Agent"));
throw new RuntimeException("Kakao login failed: " + e.getMessage(), e);
}
}
โ ๏ธ ์ค์:
@Transactional
์ด๋ ธํ ์ด์ ์ ๋ฉ์๋ ๋ด์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ด ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋๋ก ํฉ๋๋ค. ์ค๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ๋ชจ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณ๊ฒฝ์ฌํญ์ด ๋กค๋ฐฑ๋ฉ๋๋ค.
์ด ๋ฉ์๋๋ ๋ค์๊ณผ ๊ฐ์ ๋จ๊ณ๋ก ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํฉ๋๋ค:
1. ์ธ์ฆ ์ฝ๋๋ก ์นด์นด์ค ์ก์ธ์ค ํ ํฐ ์์ฒญ
2. ์ก์ธ์ค ํ ํฐ์ผ๋ก ์นด์นด์ค ์ฌ์ฉ์ ์ ๋ณด ์์ฒญ
3. ์ฌ์ฉ์ ์ ๋ณด๋ก ํ์๊ฐ์
๋๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
4. JWT ์ก์ธ์ค ํ ํฐ ์์ฑ
5. ๋ฆฌํ๋ ์ ํ ํฐ ์์ฑ ๋ฐ ์ ์ฅ
6. ๋ก๊ทธ์ธ ์ฑ๊ณต ๋ก๊ทธ ๊ธฐ๋ก
7. ๋ก๊ทธ์ธ ์๋ต ๋ฐ์ดํฐ ๋ฐํ
/**
* ์นด์นด์ค ์ก์ธ์ค ํ ํฐ ํ๋
* ์ธ์ฆ ์ฝ๋๋ฅผ ์ฌ์ฉํ์ฌ ์นด์นด์ค API์ ์ก์ธ์ค ํ ํฐ์ ์์ฒญํฉ๋๋ค.
*/
private KakaoTokenResponse getKakaoAccessToken(String code) {
// ํ ํฐ ์์ฒญ์ ์ํ ํค๋ ์ค์
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// ํ ํฐ ์์ฒญ ํ๋ผ๋ฏธํฐ ์ค์
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code"); // OAuth 2.0 ์ธ์ฆ ์ฝ๋ ๋ฐฉ์
params.add("client_id", kakaoOAuthConfig.getClientId()); // REST API ํค
params.add("redirect_uri", kakaoOAuthConfig.getRedirectUrl()); // ๋ฆฌ๋ค์ด๋ ํธ URI
params.add("code", code); // ์ธ์ฆ ์ฝ๋
// ์นด์นด์ค์ ํ ํฐ ์์ฒญ
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
ResponseEntity<KakaoTokenResponse> response = restTemplate.postForEntity(
kakaoOAuthConfig.getTokenUrl(),
request,
KakaoTokenResponse.class);
return response.getBody();
}
์ด ๋ฉ์๋๋ ์ธ์ฆ ์ฝ๋๋ฅผ ์ฌ์ฉํ์ฌ ์นด์นด์ค ์๋ฒ์ ์ก์ธ์ค ํ ํฐ์ ์์ฒญํฉ๋๋ค. OAuth 2.0 ํ๋กํ ์ฝ์ ๋ฐ๋ผ ํ์ํ ํ๋ผ๋ฏธํฐ๋ฅผ ์ค์ ํ๊ณ , RestTemplate
์ ์ฌ์ฉํ์ฌ HTTP ์์ฒญ์ ๋ณด๋
๋๋ค.
/**
* ์นด์นด์ค ์ฌ์ฉ์ ์ ๋ณด ์์ฒญ
* ์ก์ธ์ค ํ ํฐ์ ์ฌ์ฉํ์ฌ ์นด์นด์ค API์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์์ฒญํฉ๋๋ค.
*/
private KakaoUserInfoResponse getKakaoUserInfo(String accessToken) {
// ์ฌ์ฉ์ ์ ๋ณด ์์ฒญ์ ์ํ ํค๋ ์ค์
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken); // Authorization: Bearer {์ก์ธ์ค ํ ํฐ}
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// ์นด์นด์ค์ ์ฌ์ฉ์ ์ ๋ณด ์์ฒญ
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<KakaoUserInfoResponse> response = restTemplate.exchange(
kakaoOAuthConfig.getUserInfoUrl(),
HttpMethod.POST,
request,
KakaoUserInfoResponse.class);
return response.getBody();
}
์ก์ธ์ค ํ ํฐ์ ์ฌ์ฉํ์ฌ ์นด์นด์ค API์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฉ์๋์
๋๋ค. ํค๋์ Bearer
ํ ํฐ์ ์ค์ ํ์ฌ ์ธ์ฆํ๊ณ , ์๋ต์ KakaoUserInfoResponse
๊ฐ์ฒด๋ก ๋งคํํฉ๋๋ค.
/**
* ์นด์นด์ค ์ฌ์ฉ์ ์ฒ๋ฆฌ (ํ์๊ฐ์
๋๋ ๋ก๊ทธ์ธ)
* ์นด์นด์ค ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ๊ท ํ์๊ฐ์
๋๋ ๊ธฐ์กด ํ์ ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํฉ๋๋ค.
*/
private User processKakaoUser(KakaoUserInfoResponse userInfo, String accessToken) {
// ์นด์นด์ค ID๋ก ์์
๋ก๊ทธ์ธ ์ ๋ณด ์กฐํ
String kakaoId = String.valueOf(userInfo.getId());
SocialLogin socialLogin = socialLoginMapper.findByExternalId(kakaoId, KAKAO_SOCIAL_CODE);
// ์ด๋ฏธ ์นด์นด์ค๋ก ๊ฐ์
๋ ์ฌ์ฉ์์ธ ๊ฒฝ์ฐ
if (socialLogin != null) {
// ์ก์ธ์ค ํ ํฐ ์
๋ฐ์ดํธ
socialLogin.setAccessToken(accessToken);
socialLogin.setUpdateDate(LocalDateTime.now());
socialLoginMapper.update(socialLogin);
// ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
User user = userMapper.findByUserNo(socialLogin.getUserNo());
if (user == null) {
throw new RuntimeException("User not found for social login: " + kakaoId);
}
return user;
}
// ์๋ก์ด ์ฌ์ฉ์ ๋ฑ๋ก - ์ด๋ฉ์ผ๋ก ๊ธฐ์กด ํ์ ํ์ธ
String email = userInfo.getEmail();
User existingUser = null;
if (email != null && !email.isEmpty()) {
try {
// ์ด๋ฉ์ผ๋ก ๊ธฐ์กด ํ์ ์กฐํ
existingUser = userMapper.findByEmail(email);
} catch (Exception e) {
// ์ด๋ฉ์ผ๋ก ๊ฒ์ ์คํจํด๋ ๊ณ์ ์งํ
}
}
// ๊ฐ์ ์ด๋ฉ์ผ๋ก ๊ฐ์
๋ ์ฌ์ฉ์๊ฐ ์๋ ๊ฒฝ์ฐ, ์์
๋ก๊ทธ์ธ ์ ๋ณด๋ง ์ถ๊ฐ
if (existingUser != null) {
SocialLogin newSocialLogin = SocialLogin.builder()
.userNo(existingUser.getUserNo())
.socialCode(KAKAO_SOCIAL_CODE)
.externalId(kakaoId)
.accessToken(accessToken)
.updateDate(LocalDateTime.now())
.build();
// ์์
๋ก๊ทธ์ธ ์ ๋ณด ์ ์ฅ
socialLoginMapper.save(newSocialLogin);
return existingUser;
}
// ์์ ํ ์๋ก์ด ์ฌ์ฉ์์ธ ๊ฒฝ์ฐ - ํ์๊ฐ์
์ฒ๋ฆฌ
String nickname = userInfo.getNickname();
if (nickname == null || nickname.isEmpty()) {
nickname = "kakao_user_" + UUID.randomUUID().toString().substring(0, 8);
}
// ์ ๊ท ์ฌ์ฉ์ ์์ฑ
User newUser = User.builder()
.userName(nickname)
.loginType(1) // ์์
๋ก๊ทธ์ธ
.email(email)
.build();
// ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ
userMapper.save(newUser);
Long userNo = userMapper.getLastInsertId();
newUser.setUserNo(userNo);
// ์์
๋ก๊ทธ์ธ ์ ๋ณด ์ ์ฅ
SocialLogin newSocialLogin = SocialLogin.builder()
.userNo(userNo)
.socialCode(KAKAO_SOCIAL_CODE)
.externalId(kakaoId)
.accessToken(accessToken)
.updateDate(LocalDateTime.now())
.build();
socialLoginMapper.save(newSocialLogin);
return newUser;
}
์ด ๋ฉ์๋๋ ์นด์นด์ค ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฒ๋ฆฌํ์ฌ ์ธ ๊ฐ์ง ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค:
๐ TIP: ์ด๋ฉ์ผ ๊ธฐ๋ฐ์ผ๋ก ๊ณ์ ์ ํตํฉํ๋ฉด ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ ๋ฐฉ์(์ผ๋ฐ ๋ก๊ทธ์ธ, ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฑ)์ผ๋ก ๊ฐ์ ๊ณ์ ์ ์ ๊ทผํ ์ ์์ต๋๋ค.
์ด ์ธ์๋ ๋ค์๊ณผ ๊ฐ์ ๋ฉ์๋๋ค์ด ํ์ํฉ๋๋ค:
/**
* ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ
* ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฟ ํค์ ์ ์ฅํฉ๋๋ค.
*/
private void saveRefreshToken(Long userNo, String refreshToken, HttpServletResponse response) {
// DB์ ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ
RefreshToken existingToken = refreshTokenMapper.findByUserNo(userNo);
if (existingToken != null) {
// ๊ธฐ์กด ํ ํฐ์ด ์์ผ๋ฉด ์
๋ฐ์ดํธ
existingToken.setRefreshToken(refreshToken);
refreshTokenMapper.update(existingToken);
} else {
// ์ ํ ํฐ ์์ฑ
RefreshToken newToken = RefreshToken.builder()
.userNo(userNo)
.refreshToken(refreshToken)
.build();
refreshTokenMapper.save(newToken);
}
// ์ฟ ํค์ ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ
int cookieMaxAge = (int) REFRESH_TOKEN_DURATION.toSeconds();
Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken);
cookie.setPath("/"); // ๋ชจ๋ ๊ฒฝ๋ก์์ ์ฟ ํค ์ ๊ทผ ๊ฐ๋ฅ
cookie.setHttpOnly(true); // JavaScript์์ ์ฟ ํค ์ ๊ทผ ๋ถ๊ฐ (XSS ๋ฐฉ์ง)
cookie.setMaxAge(cookieMaxAge); // ์ฟ ํค ์ ํจ ๊ธฐ๊ฐ ์ค์
response.addCookie(cookie); // ์๋ต์ ์ฟ ํค ์ถ๊ฐ
}
/**
* ๋ก๊ทธ์ธ ํ๋ ๋ก๊ทธ ์ ์ฅ
*/
private void saveLog(Long userNo, String actionType, String description, String ipAddress, String userAgent) {
Log log = Log.builder()
.userNo(userNo)
.actionType(actionType)
.description(description)
.ipAddress(ipAddress)
.userAgent(userAgent != null ? userAgent : "Unknown")
.status("COMPLETED")
.createdAt(LocalDateTime.now())
.build();
logRepository.save(log);
}
/**
* ํด๋ผ์ด์ธํธ IP ์ฃผ์ ๊ฐ์ ธ์ค๊ธฐ
*/
private String getClientIp(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null) {
return request.getRemoteAddr();
}
return xfHeader.split(",")[0].trim();
}
์ด๋ ๊ฒ ์๋น์ค ํด๋์ค๋ฅผ ์์ฑํ์ต๋๋ค! ์ด์ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ค์ด ์ธ๋ถ ์์ฒญ์ ์ฒ๋ฆฌํด๋ณด๊ฒ ์ต๋๋ค.
์ปจํธ๋กค๋ฌ๋ ์ธ๋ถ HTTP ์์ฒญ์ ๋ฐ์ ์๋น์ค ๊ณ์ธต์ผ๋ก ์ ๋ฌํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ๋ ์ญํ ์ ํฉ๋๋ค.
@RestController
@RequestMapping("/api/auth/kakao")
@RequiredArgsConstructor
public class KakaoLoginController {
private final KakaoLoginService kakaoLoginService;
/**
* ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
*/
@GetMapping("/login")
public RedirectView kakaoLogin() {
String kakaoAuthUrl = kakaoLoginService.getKakaoAuthUrl();
return new RedirectView(kakaoAuthUrl);
}
/**
* ์นด์นด์ค ์ธ์ฆ ์ฝ๋ฐฑ ์ฒ๋ฆฌ
*/
@GetMapping("/callback")
public ResponseEntity<LoginResponseDto> kakaoCallback(
@RequestParam("code") String code,
HttpServletRequest request,
HttpServletResponse response) {
LoginResponseDto loginResponse = kakaoLoginService.kakaoLogin(code, request, response);
return ResponseEntity.ok(loginResponse);
}
}
์ด ์ปจํธ๋กค๋ฌ๋ ๋ ๊ฐ์ ์๋ํฌ์ธํธ๋ฅผ ์ ๊ณตํฉ๋๋ค:
๊ฐ๋จํ์ฃ ? ๋ณต์กํ ๋ก์ง์ ๋ชจ๋ ์๋น์ค ๊ณ์ธต์ ์์ํ๊ณ , ์ปจํธ๋กค๋ฌ๋ HTTP ์์ฒญ๊ณผ ์๋ต๋ง ์ฒ๋ฆฌํฉ๋๋ค.
์ด์ ๊ตฌํํ ์ฝ๋๋ฅผ ๋ฐํ์ผ๋ก ์ค์ ์นด์นด์ค ์์ ๋ก๊ทธ์ธ์ ์ ์ฒด ํ๋ฆ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์นด์นด์ค ๋ก๊ทธ์ธ ๋ฒํผ ํด๋ฆญ
/api/auth/kakao/login
์๋ํฌ์ธํธ๋ก ์์ฒญ์ ๋ณด๋
๋๋ค.์นด์นด์ค ์ธ์ฆ ํ์ด์ง ํ์
๊ถํ ๋์
๋ฐฑ์๋ ์ฒ๋ฆฌ ๋ฐ ๋ก๊ทธ์ธ ์๋ฃ
ํด๋ผ์ด์ธํธ ์ธก ์ฒ๋ฆฌ
์นด์นด์ค ๋ก๊ทธ์ธ์ด ์ด๋ป๊ฒ ์๋ํ๋์ง ์ข ๋ ๊ธฐ์ ์ ์ธ ๊ด์ ์์ ์ดํด๋ณด๊ฒ ์ต๋๋ค:
๋ก๊ทธ์ธ ์ด๊ธฐํ (ํ๋ก ํธ์๋ โ ๋ฐฑ์๋ โ ์นด์นด์ค)
GET /api/auth/kakao/login โ
302 Redirect โ
https://kauth.kakao.com/oauth/authorize?client_id=...&redirect_uri=...&response_type=code
์ธ์ฆ ์ฝ๋ ์์ (์นด์นด์ค โ ๋ฐฑ์๋)
GET /api/auth/kakao/callback?code=AUTHORIZATION_CODE
์ก์ธ์ค ํ ํฐ ์์ฒญ (๋ฐฑ์๋ โ ์นด์นด์ค)
POST https://kauth.kakao.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id=REST_API_KEY&
redirect_uri=CALLBACK_URL&
code=AUTHORIZATION_CODE
์ก์ธ์ค ํ ํฐ ์๋ต (์นด์นด์ค โ ๋ฐฑ์๋)
{
"token_type": "bearer",
"access_token": "ACCESS_TOKEN",
"refresh_token": "REFRESH_TOKEN",
"expires_in": 21599
}
์ฌ์ฉ์ ์ ๋ณด ์์ฒญ (๋ฐฑ์๋ โ ์นด์นด์ค)
POST https://kapi.kakao.com/v2/user/me
Authorization: Bearer ACCESS_TOKEN
์ฌ์ฉ์ ์ ๋ณด ์๋ต (์นด์นด์ค โ ๋ฐฑ์๋)
{
"id": 1234567890,
"connected_at": "2023-06-15T13:45:22Z",
"properties": {
"nickname": "ํ๊ธธ๋",
"profile_image": "https://..."
},
"kakao_account": {
"email": "user@example.com"
}
}
JWT ํ ํฐ ์๋ต (๋ฐฑ์๋ โ ํ๋ก ํธ์๋)
{
"userId": 123,
"username": "ํ๊ธธ๋",
"token": "JWT_ACCESS_TOKEN"
}
์ ์ฒด ์์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ฒ ์ต๋๋ค:
์๋ฒ ์คํ
./mvnw spring-boot:run
์นด์นด์ค ๋ก๊ทธ์ธ URL ์ ์
http://localhost:8080/api/auth/kakao/login
์ ์์ธ์ฆ ๊ณผ์ ํ์ธ
์๋ต ํ์ธ
๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ธ
๋ก๊ทธ์ธ ๊ตฌํ ์ค ๋ฐ์ํ ์ ์๋ ์ผ๋ฐ์ ์ธ ๋ฌธ์ ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋๋ค:
๋ฆฌ๋ค์ด๋ ํธ URI ๋ถ์ผ์น ์ค๋ฅ
ํ ํฐ ์์ฒญ ์คํจ
์ฌ์ฉ์ ์ ๋ณด ์์ฒญ ์คํจ
๋ฐ์ดํฐ ๋งคํ ์ค๋ฅ
๋ก๊ทธ๋ฅผ ์ ๊ทน ํ์ฉํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ธ์:
log.debug("์ธ์ฆ ์ฝ๋: {}", code);
log.debug("ํ ํฐ ์๋ต: {}", tokenResponse);
log.debug("์ฌ์ฉ์ ์ ๋ณด: {}", userInfo);
์ด์ ๊ธฐ๋ณธ์ ์ธ ์นด์นด์ค ์์ ๋ก๊ทธ์ธ ๊ตฌํ์ด ์๋ฃ๋์์ต๋๋ค! ํ์ง๋ง ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ ๊ณ ๋ คํด์ผ ํ ๋ถ๋ถ๋ค์ด ์์ต๋๋ค.
// HTTPS ์ ์ฉ ์ฟ ํค ์ค์
cookie.setSecure(true); // HTTPS์์๋ง ์ฟ ํค ์ ์ก
cookie.setSameSite("Strict"); // CSRF ๋ฐฉ์ง
// ์ก์ธ์ค ํ ํฐ ์์ ํ๊ฒ ์ ์ฅ
// ํ๋ก ํธ์๋์์๋ LocalStorage ๋์ HttpOnly ์ฟ ํค๋ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์
// CSRF ๋ณดํธ ๊ตฌํ
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
// ํ ํฐ ๋ง๋ฃ ์๊ฐ ๊ด๋ฆฌ
private void validateTokenExpiration() {
// ์ฃผ๊ธฐ์ ์ผ๋ก ๋ง๋ฃ๋ ํ ํฐ ์ ๋ฆฌ
}
### 2๏ธโฃ ๋ค๋ฅธ ์์
๋ก๊ทธ์ธ ์ถ๊ฐํ๊ธฐ
์นด์นด์ค ๋ก๊ทธ์ธ ๊ตฌํ ํจํด์ ํ์ฉํ๋ฉด ๋ค๋ฅธ ์์
๋ก๊ทธ์ธ๋ ์ฝ๊ฒ ์ถ๊ฐํ ์ ์์ต๋๋ค.
```java
// ๊ณตํต ์ธํฐํ์ด์ค ์ ์
public interface SocialLoginService {
String getAuthorizationUrl();
LoginResponseDto login(String code, HttpServletRequest request, HttpServletResponse response);
}
// ์นด์นด์ค ๋ก๊ทธ์ธ ์๋น์ค
@Service
public class KakaoLoginServiceImpl implements SocialLoginService {
// ๊ตฌํ...
}
// ๋ค์ด๋ฒ ๋ก๊ทธ์ธ ์๋น์ค
@Service
public class NaverLoginServiceImpl implements SocialLoginService {
// ๊ตฌํ...
}
// ๊ตฌ๊ธ ๋ก๊ทธ์ธ ์๋น์ค
@Service
public class GoogleLoginServiceImpl implements SocialLoginService {
// ๊ตฌํ...
}
์์ ๋ก๊ทธ์ธ ์ดํ์ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ธฐ ์ํ ๋ช ๊ฐ์ง ์์ด๋์ด:
ํ๋กํ ์ ๋ณด ๋๊ธฐํ
๊ณ์ ์ฐ๋ ๊ด๋ฆฌ
๋ก๊ทธ์ธ ์คํจ ์ฒ๋ฆฌ
์๋ ๋ก๊ทธ์ธ
// ์์
ํ๋กํ ๋๊ธฐํ ์์
@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // ํ๋ฃจ์ ํ ๋ฒ
public void synchronizeProfiles() {
List<SocialLogin> kakaoLogins = socialLoginMapper.findAllBySocialCode(KAKAO_SOCIAL_CODE);
for (SocialLogin login : kakaoLogins) {
try {
// ์ต์ ํ๋กํ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
KakaoUserInfoResponse userInfo = getKakaoUserInfo(login.getAccessToken());
// ํ๋กํ ์ ๋ณด ์
๋ฐ์ดํธ
User user = userMapper.findByUserNo(login.getUserNo());
if (user != null && userInfo.getNickname() != null) {
user.setUsername(userInfo.getNickname());
userMapper.update(user);
}
} catch (Exception e) {
log.error("ํ๋กํ ๋๊ธฐํ ์คํจ: {}", login.getUserNo(), e);
}
}
}
๋ง์ง๋ง์ผ๋ก, ์์ ๋ก๊ทธ์ธ ๊ตฌํ์ ์์ ์ฑ์ ๋์ด๊ธฐ ์ํ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค.
@SpringBootTest
@AutoConfigureMockMvc
public class KakaoLoginControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private KakaoLoginService kakaoLoginService;
@Test
public void testKakaoLogin_ShouldRedirectToKakaoAuthUrl() throws Exception {
// Given
String kakaoAuthUrl = "https://kauth.kakao.com/oauth/authorize?client_id=...";
when(kakaoLoginService.getKakaoAuthUrl()).thenReturn(kakaoAuthUrl);
// When & Then
mockMvc.perform(get("/api/auth/kakao/login"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl(kakaoAuthUrl));
}
@Test
public void testKakaoCallback_ShouldReturnLoginResponse() throws Exception {
// Given
String code = "test_auth_code";
LoginResponseDto loginResponse = new LoginResponseDto(1L, "testUser", "jwt_token");
when(kakaoLoginService.kakaoLogin(eq(code), any(), any())).thenReturn(loginResponse);
// When & Then
mockMvc.perform(get("/api/auth/kakao/callback")
.param("code", code))
.andExpect(status().isOk())
.andExpect(jsonPath("$.userId").value(1))
.andExpect(jsonPath("$.username").value("testUser"))
.andExpect(jsonPath("$.token").value("jwt_token"));
}
}
์ง๊ธ๊น์ง ์คํ๋ง๋ถํธ์์ ์นด์นด์ค ์์ ๋ก๊ทธ์ธ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์ธํ ์์๋ณด์์ต๋๋ค. ๊ธฐ๋ณธ์ ์ธ ๊ตฌํ๋ถํฐ ์ค์ ์๋น์ค์ ์ ์ฉํ๊ธฐ ์ํ ์ถ๊ฐ ๊ณ ๋ ค์ฌํญ๊น์ง ๋ค๋ฃจ์์ต๋๋ค.
์์ ๋ก๊ทธ์ธ์ ์ฌ์ฉ์์๊ฒ ํธ๋ฆฌํ ๋ก๊ทธ์ธ ๊ฒฝํ์ ์ ๊ณตํ ๋ฟ๋ง ์๋๋ผ, ๊ฐ๋ฐ์ ์ ์ฅ์์๋ ์ธ์ฆ ๋ก์ง์ ๊ฐ์ํํ๊ณ ๋ณด์ ๋ถ๋ด์ ์ค์ผ ์ ์๋ ์ข์ ๋ฐฉ๋ฒ์ ๋๋ค.
๋ค์ ๊ธ์์๋ ํ๋ก ํธ์๋์์ ์์ ๋ก๊ทธ์ธ ๊ฒฐ๊ณผ๋ฅผ ์ฒ๋ฆฌํ๊ณ JWT ํ ํฐ์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ํด ๋ค๋ฃจ๋๋ก ํ๊ฒ ์ต๋๋ค. ์ง๋ฌธ์ด๋ ์๊ฒฌ์ด ์์ผ์๋ฉด ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์! ๐