[JWT+OAuth2] Spring Security를 활용한 Kakao 로그인 구현

이재민·2025년 1월 25일
0

JPA 

목록 보기
21/21
post-thumbnail

1. 서론

Google, Github 로그인 구현 이후 카카오톡 로그인을 구현해보도록 하겠습니다.

카카오 로그인은 다음과 같은 로직으로 진행됩니다.

또한 카카오 로그인은 CommonOAuth2Provider에 따로 등록되어있지 않아서 따로 작업을 해주어야 합니다.

2. 사전 작업

1. 카카오 디벨로퍼스에 들어갑니다.

2. 애플리케이션을 만들어줍니다.

3. 앱 키에서 REST API 키를 복사해줍니다.

4. 그리고 동의항목에 총 3개를 동의해줍니다.

5. 카카오 로그인 ON을 해준 뒤 Redirect URI에 값을 작성해줍니다.

값 : http://localhost:8080/login/oauth2/code/kakao

6. application.properties에 값을 넣어줍니다.

spring.security.oauth2.client.registration.kakao.client-id=${KAKAO_CLIENT_ID:0~}
spring.security.oauth2.client.registration.kakao.redirect-uri=${KAKAO_REDIRECT_URI:http://localhost:8080/login/oauth2/code/kakao}
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,profile_image,account_email
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id

이렇게 하면 사전 작업은 끝났습니다!

3. 카카오 로그인 구현 전체 흐름 및 코드


서론에 넣어놨던 사진을 다시 보며 step별로 진행해보도록 하겠습니다.


Step 1: 카카오 로그인

1-1. 사용자 클라이언트 요청

사용자가 카카오 로그인 버튼을 클릭하면 인가 코드를 받기 위한 URL로 리디렉션됩니다. 이 과정은 클라이언트(Vue.js)와 Spring Boot 백엔드 간의 상호작용으로 시작됩니다.


1-2. 인가 코드 발급 요청

KakaoUtil
카카오 인증 서버에 요청을 보내고, 인가 코드를 통해 액세스 토큰을 발급받습니다.

@Component
@Slf4j
@RequiredArgsConstructor
public class KakaoUtil {

    private final ObjectMapper objectMapper;

    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    private String clientId;

    @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
    private String redirectUri;

    public KakaoDTO.OAuthToken requestToken(String accessCode) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("redirect_uri", redirectUri);
        params.add("code", accessCode);

        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(params, headers);

        ResponseEntity<String> response = restTemplate.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                kakaoTokenRequest,
                String.class);

        KakaoDTO.OAuthToken oAuthToken;

        try {
            oAuthToken = objectMapper.readValue(response.getBody(), KakaoDTO.OAuthToken.class);
            log.info("oAuthToken : " + oAuthToken.getAccess_token());
        } catch (JsonProcessingException e) {
            throw new AuthHandler(ErrorStatus._PARSING_ERROR);
        }

        return oAuthToken;
    }
}

Step 2: 회원 확인 및 가입

2-1. 액세스 토큰으로 사용자 정보 요청

KakaoUtil
카카오 API 서버에 요청하여 사용자 정보를 가져옵니다.

public KakaoDTO.KakaoProfile requestProfile(KakaoDTO.OAuthToken oAuthToken) {
    RestTemplate restTemplate2 = new RestTemplate();
    HttpHeaders headers2 = new HttpHeaders();

    headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
    headers2.add("Authorization", "Bearer " + oAuthToken.getAccess_token());

    HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers2);

    ResponseEntity<String> response2 = restTemplate2.exchange(
            "https://kapi.kakao.com/v2/user/me",
            HttpMethod.GET,
            kakaoProfileRequest,
            String.class);

    KakaoDTO.KakaoProfile kakaoProfile;

    try {
        kakaoProfile = objectMapper.readValue(response2.getBody(), KakaoDTO.KakaoProfile.class);
    } catch (JsonProcessingException e) {
        log.info(Arrays.toString(e.getStackTrace()));
        throw new AuthHandler(ErrorStatus._PARSING_ERROR);
    }

    return kakaoProfile;
}

2-2. 사용자 정보로 회원 확인

AuthService
액세스 토큰으로 가져온 사용자 정보를 통해 회원 여부를 확인하거나 신규 회원을 등록합니다.

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {

    private final KakaoUtil kakaoUtil;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;
    private final PasswordEncoder passwordEncoder;

    public User oAuthLogin(String accessCode, HttpServletResponse httpServletResponse) {
        KakaoDTO.OAuthToken oAuthToken = kakaoUtil.requestToken(accessCode);
        KakaoDTO.KakaoProfile kakaoProfile = kakaoUtil.requestProfile(oAuthToken);
        String email = kakaoProfile.getKakao_account().getEmail();

        User user = userRepository.findByEmail(email)
                .orElseGet(() -> createNewUser(kakaoProfile));

        String token = jwtUtil.createAccessToken(user.getEmail(), user.getRole().toString());
        httpServletResponse.setHeader("Authorization", token);

        return user;
    }

    private User createNewUser(KakaoDTO.KakaoProfile kakaoProfile) {
        User newUser = AuthConverter.toUser(
                kakaoProfile.getKakao_account().getEmail(),
                kakaoProfile.getKakao_account().getProfile().getNickname(),
                null,
                passwordEncoder
        );
        return userRepository.save(newUser);
    }
}

Step 3: 서비스 로그인

3-1. JWT 생성 및 응답

JwtUtil
사용자 이메일과 역할(Role)을 기반으로 JWT를 생성합니다.

public class JwtUtil {

    private final String secretKey;
    private static final long EXPIRATION_TIME = 86400000; // 1일 (밀리초)

    public JwtUtil(String secretKey) {
        this.secretKey = secretKey;
    }

    public String createAccessToken(String email, String role) {
        return Jwts.builder()
                .setSubject(email)
                .claim("role", role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
}

3-2. 클라이언트 응답

AuthController
카카오 로그인 요청에 대한 최종 응답을 처리합니다.

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final AuthService authService;

    @GetMapping("login/oauth2/code/kakao")
    public BaseResponse<UserResponseDTO.JoinResultDTO> kakaoLogin(@RequestParam("code") String accessCode, HttpServletResponse httpServletResponse) {
        User user = authService.oAuthLogin(accessCode, httpServletResponse);
        return BaseResponse.onSuccess(AuthConverter.toJoinResultDTO(user));
    }
}

사용된 기타 클래스

주요 로직에 포함되어 있지 않지만 기능 구현에 필요한 기타 클래스를 정리해보았습니다.

1. AuthConverter

DTO 변환을 담당합니다.

public class AuthConverter {

    public static User toUser(String email, String name, String password, PasswordEncoder passwordEncoder) {
        return User.builder()
                .email(email)
                .role("ROLE_USER")
                .password(passwordEncoder.encode(password != null ? password : "default"))
                .name(name)
                .build();
    }

    public static UserResponseDTO.JoinResultDTO toJoinResultDTO(User user) {
        return UserResponseDTO.JoinResultDTO.builder()
                .email(user.getEmail())
                .name(user.getName())
                .token("Bearer <JWT_TOKEN>")
                .build();
    }
}

2. User

사용자 엔티티입니다.

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String role;
}

3. UserRequestDTO

요청 데이터를 처리하기 위한 DTO입니다.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserRequestDTO {
    private String email;
    private String name;
    private String password;

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class LoginRequestDTO {
        private String email;
        private String password;
    }
}

4. UserResponseDTO

응답 데이터를 처리하기 위한 DTO입니다.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserResponseDTO {

    private String email;
    private String name;

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class JoinResultDTO {
        private String email;
        private String name;
        private String token;
    }
}

5. KakaoDTO

카카오 API의 응답 데이터를 매핑합니다.

public class KakaoDTO {


    @Getter
    public static class OAuthToken {
        private String access_token;
        private String token_type;
        private String refresh_token;
        private int expires_in;
        private String scope;
        private int refresh_token_expires_in;
    }

    @Getter
    public static class KakaoProfile {
        private Long id;
        private String connected_at;
        private Properties properties;
        private KakaoAccount kakao_account;

        @Getter
        public class Properties {
            private String nickname;
        }

        @Getter
        public class KakaoAccount {
            private String email;
            private Boolean is_email_verified;
            private Boolean has_email;
            private Boolean profile_nickname_needs_agreement;
            private Boolean email_needs_agreement;
            private Boolean is_email_valid;
            private Profile profile;

            @Getter
            public class Profile {
                private String nickname;
                private Boolean is_default_nickname;
            }
        }
    }
}

최종 코드 정리

지금까지 한 클래스들을 정리해보겠습니다.

핵심 흐름 코드

  1. KakaoUtil: 카카오 API 서버와 통신(인가 코드 요청, 토큰 발급, 사용자 정보 요청).
  2. AuthService: 사용자의 회원 여부를 확인하고, 신규 사용자는 등록하며 JWT 생성.
  3. JwtUtil: JWT 생성 로직을 담당.
  4. AuthController: 카카오 로그인 요청을 처리하고 최종적으로 JWT를 클라이언트에 응답.

DTO 및 엔티티

  1. KakaoDTO: 카카오 API에서 반환된 JSON 응답 데이터를 매핑.
  2. UserResponseDTO: 클라이언트 응답 데이터를 정의.
  3. UserRequestDTO: 클라이언트 요청 데이터를 정의.
  4. AuthConverter: User 엔티티와 DTO 간의 변환 로직 제공.
  5. User: 데이터베이스에 저장될 사용자 엔티티.

4. 실행 화면

일단 package.json에서 vue-cli-service serve를 통해 vue.js 서버를 띄웁니다.


 DONE  Compiled successfully in 906ms        3:02:41 PM
  App running at:
  - Local:   http://localhost:8081/ 
  - Network: http://172.30.1.1:8081/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

스프링을 8080으로 띄우고 vue.js를 8081로 띄어주었습니다.


그리고 localhost:8080으로 들어가면 /login으로 redircetion이 됩니다.

이후 카카오 로그인을 한 뒤 들어가보면

다음과 같은 화면이 뜨고 리다이렉트 url이 보인다면 성공입니다 !

추가적으로 로그를 보면

2025-01-25T16:11:01.617+09:00 DEBUG 98789 --- [springsecOAUTH2] [nio-8080-exec-7] .s.o.c.w.OAuth2LoginAuthenticationFilter : Set SecurityContextHolder to OAuth2AuthenticationToken [Principal=Name: [3884704634], Granted Authorities: [[OAUTH2_USER, SCOPE_account_email, SCOPE_profile_image, SCOPE_profile_nickname]], 
User Attributes: [{id=3~, connected_at=2025-01-19T08:09:20Z, properties={nickname=이xx, profile_image=http://img1.kakaocdn.net/thumb/R...q70/? ...

이런식으로 나오면 성공입니다.

여기서 이제 로그아웃 기능과 커스텀을 추가해서 하면 됩니다!

지금까지 카카오 로그인 구현을 살펴보았습니다.
전체 코드 관련해서는
SocialLogin <- 여기를 참고하시면 되겠습니다!

참고 링크 : [SpringBoot] Rest API 카카오 (Kakao) OAuth 로그인 구현하기

profile
복학생의 개발 일기

0개의 댓글

관련 채용 정보