๐Ÿ” ์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ ์นด์นด์˜ค ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ - 2. ์„œ๋น„์Šค ๋ฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„

๋‚˜๊ทผ๋ฏผยท2025๋…„ 4์›” 4์ผ
0

๐Ÿ” ์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ ์นด์นด์˜ค ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ - 2. ์„œ๋น„์Šค ๋ฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„

์•ˆ๋…•ํ•˜์„ธ์š”! ์ง€๋‚œ ํฌ์ŠคํŒ…์—์„œ๋Š” ์นด์นด์˜ค ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์„ค์ •๊ณผ DTO ํด๋ž˜์Šค๋“ค์„ ์•Œ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹ค์ œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค์™€ ์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”.

์ด ๊ธ€์„ ํ†ตํ•ด OAuth 2.0 ์ธ์ฆ ํ๋ฆ„์„ ์ดํ•ดํ•˜๊ณ , ์นด์นด์˜ค API์™€ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ• ๋“ฑ์„ ์ž์„ธํžˆ ์•Œ์•„๋ด…์‹œ๋‹ค! ๐Ÿš€

๐Ÿ“‹ ๋ชฉ์ฐจ

  1. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„œ๋น„์Šค ๊ตฌํ˜„ํ•˜๊ธฐ
  2. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ํ•˜๊ธฐ
  3. ์ „์ฒด ๋กœ๊ทธ์ธ ํ๋ฆ„ ์‚ดํŽด๋ณด๊ธฐ
  4. ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•
  5. ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„

์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„œ๋น„์Šค ๊ตฌํ˜„ํ•˜๊ธฐ

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

1๏ธโƒฃ ์„œ๋น„์Šค ํด๋ž˜์Šค ๊ธฐ๋ณธ ๊ตฌ์กฐ

@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 ํ•„๋“œ์— ๋Œ€ํ•œ ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๋กฌ๋ณต ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. ์Šคํ”„๋ง์˜ ์ƒ์„ฑ์ž ๊ธฐ๋ฐ˜ ์˜์กด์„ฑ ์ฃผ์ž…์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2๏ธโƒฃ ์นด์นด์˜ค ์ธ์ฆ URL ์ƒ์„ฑ ๋ฉ”์„œ๋“œ

/**
 * ์นด์นด์˜ค ์ธ์ฆ URL ๋ฐ˜ํ™˜
 * ์‚ฌ์šฉ์ž๋ฅผ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๊ธฐ ์œ„ํ•œ URL์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
 */
public String getKakaoAuthUrl() {
    return kakaoOAuthConfig.getAuthorizationUrl();
}

์ด ๋ฉ”์„œ๋“œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค์ • ํด๋ž˜์Šค์—์„œ ์ƒ์„ฑํ•œ ์ธ์ฆ URL์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด URL๋กœ ์‚ฌ์šฉ์ž๋ฅผ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

3๏ธโƒฃ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ

์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค.

/**
 * ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
 * ์นด์นด์˜ค ์ธ์ฆ ํ›„ ๋ฐ›์€ ์ฝ”๋“œ๋กœ ์•ก์„ธ์Šค ํ† ํฐ์„ ์š”์ฒญํ•˜๊ณ , ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜์—ฌ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
 */
@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. ๋กœ๊ทธ์ธ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜

4๏ธโƒฃ ์นด์นด์˜ค ์•ก์„ธ์Šค ํ† ํฐ ์š”์ฒญ ๋ฉ”์„œ๋“œ

/**
 * ์นด์นด์˜ค ์•ก์„ธ์Šค ํ† ํฐ ํš๋“
 * ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์นด์นด์˜ค 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 ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

5๏ธโƒฃ ์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ ๋ฉ”์„œ๋“œ

/**
 * ์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ
 * ์•ก์„ธ์Šค ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ์นด์นด์˜ค 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 ๊ฐ์ฒด๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค.

6๏ธโƒฃ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ

/**
 * ์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ (ํšŒ์›๊ฐ€์ž… ๋˜๋Š” ๋กœ๊ทธ์ธ)
 * ์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‹ ๊ทœ ํšŒ์›๊ฐ€์ž… ๋˜๋Š” ๊ธฐ์กด ํšŒ์› ๋กœ๊ทธ์ธ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
 */
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;
}

์ด ๋ฉ”์„œ๋“œ๋Š” ์นด์นด์˜ค ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ ์„ธ ๊ฐ€์ง€ ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค:

  1. ์ด๋ฏธ ์นด์นด์˜ค๋กœ ๋กœ๊ทธ์ธํ•œ ์ ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž - ์•ก์„ธ์Šค ํ† ํฐ๋งŒ ์—…๋ฐ์ดํŠธ
  2. ๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž - ์†Œ์…œ ๋กœ๊ทธ์ธ ์ •๋ณด๋งŒ ์ถ”๊ฐ€
  3. ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž - ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰

๐Ÿ” TIP: ์ด๋ฉ”์ผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ„์ •์„ ํ†ตํ•ฉํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๋ฐฉ์‹(์ผ๋ฐ˜ ๋กœ๊ทธ์ธ, ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋“ฑ)์œผ๋กœ ๊ฐ™์€ ๊ณ„์ •์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

7๏ธโƒฃ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ฐ ๋กœ๊ทธ ๊ด€๋ จ ๋ฉ”์„œ๋“œ

์ด ์™ธ์—๋„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋“ค์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

/**
 * ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ €์žฅ
 * ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์ฟ ํ‚ค์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
 */
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);
    }
}

์ด ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋‘ ๊ฐœ์˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

  1. GET /api/auth/kakao/login - ์‚ฌ์šฉ์ž๋ฅผ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•ฉ๋‹ˆ๋‹ค.
  2. GET /api/auth/kakao/callback - ์นด์นด์˜ค ์ธ์ฆ ํ›„ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋Š” URL๋กœ, ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•˜์ฃ ? ๋ณต์žกํ•œ ๋กœ์ง์€ ๋ชจ๋‘ ์„œ๋น„์Šค ๊ณ„์ธต์— ์œ„์ž„ํ•˜๊ณ , ์ปจํŠธ๋กค๋Ÿฌ๋Š” HTTP ์š”์ฒญ๊ณผ ์‘๋‹ต๋งŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ „์ฒด ๋กœ๊ทธ์ธ ํ๋ฆ„ ์‚ดํŽด๋ณด๊ธฐ

์ด์ œ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์ œ ์นด์นด์˜ค ์†Œ์…œ ๋กœ๊ทธ์ธ์˜ ์ „์ฒด ํ๋ฆ„์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

1๏ธโƒฃ ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ์˜ ๋กœ๊ทธ์ธ ํ๋ฆ„

  1. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ

    • ์‚ฌ์šฉ์ž๊ฐ€ ์›น์‚ฌ์ดํŠธ์—์„œ "์นด์นด์˜ค๋กœ ๋กœ๊ทธ์ธ" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค.
    • ๋ธŒ๋ผ์šฐ์ €๋Š” /api/auth/kakao/login ์—”๋“œํฌ์ธํŠธ๋กœ ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  2. ์นด์นด์˜ค ์ธ์ฆ ํŽ˜์ด์ง€ ํ‘œ์‹œ

    • ๋ฐฑ์—”๋“œ๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•ฉ๋‹ˆ๋‹ค.
    • ์‚ฌ์šฉ์ž๋Š” ์นด์นด์˜ค ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ๋กœ๊ทธ์ธํ•ฉ๋‹ˆ๋‹ค.
  3. ๊ถŒํ•œ ๋™์˜

    • ์ฒ˜์Œ ๋กœ๊ทธ์ธํ•˜๋Š” ๊ฒฝ์šฐ, ์นด์นด์˜ค๋Š” ์•ฑ์ด ์š”์ฒญํ•˜๋Š” ์ •๋ณด(๋‹‰๋„ค์ž„, ์ด๋ฉ”์ผ ๋“ฑ)์— ๋Œ€ํ•œ ๋™์˜๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.
    • ์‚ฌ์šฉ์ž๊ฐ€ ๋™์˜ํ•˜๋ฉด ์นด์นด์˜ค๋Š” ์ธ์ฆ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ๋ฐฑ์—”๋“œ์˜ ์ฝœ๋ฐฑ URL๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•ฉ๋‹ˆ๋‹ค.
  4. ๋ฐฑ์—”๋“œ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ๊ทธ์ธ ์™„๋ฃŒ

    • ๋ฐฑ์—”๋“œ๋Š” ์ธ์ฆ ์ฝ”๋“œ๋กœ ์•ก์„ธ์Šค ํ† ํฐ์„ ์š”์ฒญํ•˜๊ณ , ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    • ๊ธฐ์กด ์‚ฌ์šฉ์ž๋ฉด ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ๋ฅผ, ์‹ ๊ทœ ์‚ฌ์šฉ์ž๋ฉด ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.
    • JWT ํ† ํฐ์„ ์ƒ์„ฑํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  5. ํด๋ผ์ด์–ธํŠธ ์ธก ์ฒ˜๋ฆฌ

    • ํ”„๋ก ํŠธ์—”๋“œ๋Š” JWT ํ† ํฐ์„ ๋ฐ›์•„ ์ €์žฅํ•˜๊ณ , ์ดํ›„ API ์š”์ฒญ์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ณ , ๋กœ๊ทธ์ธ๋œ ์ƒํƒœ๋กœ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

2๏ธโƒฃ ๊ธฐ์ˆ ์  ๊ด€์ ์—์„œ์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„

์นด์นด์˜ค ๋กœ๊ทธ์ธ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ์ข€ ๋” ๊ธฐ์ˆ ์ ์ธ ๊ด€์ ์—์„œ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

  1. ๋กœ๊ทธ์ธ ์ดˆ๊ธฐํ™” (ํ”„๋ก ํŠธ์—”๋“œ โ†’ ๋ฐฑ์—”๋“œ โ†’ ์นด์นด์˜ค)

    GET /api/auth/kakao/login โ†’ 
    302 Redirect โ†’ 
    https://kauth.kakao.com/oauth/authorize?client_id=...&redirect_uri=...&response_type=code
  2. ์ธ์ฆ ์ฝ”๋“œ ์ˆ˜์‹  (์นด์นด์˜ค โ†’ ๋ฐฑ์—”๋“œ)

    GET /api/auth/kakao/callback?code=AUTHORIZATION_CODE
  3. ์•ก์„ธ์Šค ํ† ํฐ ์š”์ฒญ (๋ฐฑ์—”๋“œ โ†’ ์นด์นด์˜ค)

    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
  4. ์•ก์„ธ์Šค ํ† ํฐ ์‘๋‹ต (์นด์นด์˜ค โ†’ ๋ฐฑ์—”๋“œ)

    {
      "token_type": "bearer",
      "access_token": "ACCESS_TOKEN",
      "refresh_token": "REFRESH_TOKEN",
      "expires_in": 21599
    }
  5. ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ (๋ฐฑ์—”๋“œ โ†’ ์นด์นด์˜ค)

    POST https://kapi.kakao.com/v2/user/me
    Authorization: Bearer ACCESS_TOKEN
  6. ์‚ฌ์šฉ์ž ์ •๋ณด ์‘๋‹ต (์นด์นด์˜ค โ†’ ๋ฐฑ์—”๋“œ)

    {
      "id": 1234567890,
      "connected_at": "2023-06-15T13:45:22Z",
      "properties": {
        "nickname": "ํ™๊ธธ๋™",
        "profile_image": "https://..."
      },
      "kakao_account": {
        "email": "user@example.com"
      }
    }
  7. JWT ํ† ํฐ ์‘๋‹ต (๋ฐฑ์—”๋“œ โ†’ ํ”„๋ก ํŠธ์—”๋“œ)

    {
      "userId": 123,
      "username": "ํ™๊ธธ๋™",
      "token": "JWT_ACCESS_TOKEN"
    }

ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•

1๏ธโƒฃ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์ „์ฒด ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

  1. ์„œ๋ฒ„ ์‹คํ–‰

    ./mvnw spring-boot:run
  2. ์นด์นด์˜ค ๋กœ๊ทธ์ธ URL ์ ‘์†

    • ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8080/api/auth/kakao/login ์ ‘์†
  3. ์ธ์ฆ ๊ณผ์ • ํ™•์ธ

    • ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ
    • ๋กœ๊ทธ์ธ ์‹œ๋„ ํ›„ ๊ถŒํ•œ ๋™์˜ ํŽ˜์ด์ง€๊ฐ€ ๋‚˜ํƒ€๋‚˜๋Š”์ง€ ํ™•์ธ
    • ์ฝœ๋ฐฑ URL๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋Š”์ง€ ํ™•์ธ
  4. ์‘๋‹ต ํ™•์ธ

    • ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ ๋„คํŠธ์›Œํฌ ํƒญ์„ ์—ด์–ด ์‘๋‹ต ํ™•์ธ
    • HTTP 200 ์‘๋‹ต ์ฝ”๋“œ์™€ JSON ๋ฐ์ดํ„ฐ ํ™•์ธ
    • JWT ํ† ํฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธ (jwt.io์—์„œ ๋””์ฝ”๋”ฉ ๊ฐ€๋Šฅ)
  5. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™•์ธ

    • ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”์— ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    • ์†Œ์…œ ๋กœ๊ทธ์ธ ํ…Œ์ด๋ธ”์— ์—ฐ๊ฒฐ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

2๏ธโƒฃ ๋””๋ฒ„๊น… ํŒ

๋กœ๊ทธ์ธ ๊ตฌํ˜„ ์ค‘ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค:

  1. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜

    • ์นด์นด์˜ค ๊ฐœ๋ฐœ์ž ์„ผํ„ฐ์— ๋“ฑ๋กํ•œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI์™€ ์ฝ”๋“œ์˜ URI๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ๋งจ ๋์˜ ์Šฌ๋ž˜์‹œ(/)๋‚˜ ๋Œ€์†Œ๋ฌธ์ž ์ฐจ์ด๋„ ์˜ค๋ฅ˜์˜ ์›์ธ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. ํ† ํฐ ์š”์ฒญ ์‹คํŒจ

    • ์š”์ฒญ ํ—ค๋”์™€ ๋ณธ๋ฌธ ํ˜•์‹์„ ์ •ํ™•ํžˆ ํ™•์ธํ•˜์„ธ์š”.
    • Content-Type์ด application/x-www-form-urlencoded๋กœ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
    • ๋ชจ๋“  ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
  3. ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ ์‹คํŒจ

    • Authorization ํ—ค๋” ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•˜์„ธ์š”.
    • ์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•˜์„ธ์š”.
  4. ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์˜ค๋ฅ˜

    • JSON ์‘๋‹ต ๊ตฌ์กฐ์™€ DTO ํด๋ž˜์Šค์˜ ํ•„๋“œ๋ช…์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
    • ์ค‘์ฒฉ๋œ ๊ฐ์ฒด๋‚˜ ๋ฐฐ์—ด์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์„ ํ™•์ธํ•˜์„ธ์š”.

๋กœ๊ทธ๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์„ธ์š”:

log.debug("์ธ์ฆ ์ฝ”๋“œ: {}", code);
log.debug("ํ† ํฐ ์‘๋‹ต: {}", tokenResponse);
log.debug("์‚ฌ์šฉ์ž ์ •๋ณด: {}", userInfo);

๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„

์ด์ œ ๊ธฐ๋ณธ์ ์ธ ์นด์นด์˜ค ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ํ•˜์ง€๋งŒ ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ถ€๋ถ„๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค.

1๏ธโƒฃ ๋ณด์•ˆ ๊ฐ•ํ™”

// 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 {
    // ๊ตฌํ˜„...
}

3๏ธโƒฃ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

์†Œ์…œ ๋กœ๊ทธ์ธ ์ดํ›„์˜ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ์•„์ด๋””์–ด:

  1. ํ”„๋กœํ•„ ์ •๋ณด ๋™๊ธฐํ™”

    • ์นด์นด์˜ค ํ”„๋กœํ•„ ์ •๋ณด๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”
    • ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ํŽ˜์ด์ง€์—์„œ ์—ฐ๊ฒฐ๋œ ์†Œ์…œ ๊ณ„์ • ํ‘œ์‹œ
  2. ๊ณ„์ • ์—ฐ๋™ ๊ด€๋ฆฌ

    • ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ์†Œ์…œ ๊ณ„์ •์„ ํ•˜๋‚˜์˜ ๊ณ„์ •์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” UI ์ œ๊ณต
    • ์†Œ์…œ ๊ณ„์ • ์—ฐ๊ฒฐ/ํ•ด์ œ ๊ธฐ๋Šฅ
  3. ๋กœ๊ทธ์ธ ์‹คํŒจ ์ฒ˜๋ฆฌ

    • ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€
    • ์†Œ์…œ ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ ๋Œ€์ฒด ๋กœ๊ทธ์ธ ๋ฐฉ๋ฒ• ์ œ์•ˆ
  4. ์ž๋™ ๋กœ๊ทธ์ธ

    • ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ํ™œ์šฉํ•œ ์ž๋™ ๋กœ๊ทธ์ธ ๊ตฌํ˜„
    • ๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€ ์˜ต์…˜
// ์†Œ์…œ ํ”„๋กœํ•„ ๋™๊ธฐํ™” ์˜ˆ์‹œ
@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);
        }
    }
}

4๏ธโƒฃ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

๋งˆ์ง€๋ง‰์œผ๋กœ, ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„์˜ ์•ˆ์ •์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

@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 ํ† ํฐ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์งˆ๋ฌธ์ด๋‚˜ ์˜๊ฒฌ์ด ์žˆ์œผ์‹œ๋ฉด ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”! ๐Ÿ˜Š


์ฐธ๊ณ  ์ž๋ฃŒ

profile
๊ฐœ๋ฐœ ๊ณต๋ถ€์ค‘์ธ ํ•™์ƒ์ž…๋‹ˆ๋‹ค~

0๊ฐœ์˜ ๋Œ“๊ธ€