[프로젝트] Spring boot에서 jwt를 이용한 카카오 로그인 구현

chaen-ing·2024년 12월 12일
0

프로젝트

목록 보기
2/2

이번 글에서는 카카오 api를 이용한 회원가입, 로그인 + jwt 토큰 발급 과정에 대해 정리해보겠습니다.

위의 그림이 카카오 공식 문서에 나와있는 서비스 로그인 과정으로
글로 다시 적어보자면 아래와 같습니다

  1. 클라이언트가 카카오 로그인 요청
  2. 클라이언트는 카카오로부터 code를 받아 서버로 전달
  3. code를 통해 서버에서 카카오로 토큰 발급 요청
  4. 카카오는 code 등을 검증 후 토큰을 서버로 전달
  5. 서버에서 토큰으로 유저 정보 조회, 등록
  6. 서버에서 유저에게 JWT 토큰을 생성 및 전달
  7. 유저는 요청마다 jwt토큰을 포함해서 서버에 요청

차근차근..해봅시다

일단 초기세팅

https://developers.kakao.com

kakaodevelopers에 들어가서 내 애플리케이션 > 애플리케이션 추가하기
이름이랑 아이콘 이런거 설정해서 하나 만들어준다.생성한 애플리케이션으로 들어가서 아래 항목들을 설정해줘야한다.

  • 카카오 로그인 : ON으로 설정
  • 동의항목 : 비즈앱으로 등록하면 닉네임, 프로필사진, 카카오계정을 필수/선택으로 받을 수 있다. 이름, 성별, 연령대 등등은 사업자번호가 있어야한다고.. 🥹 나는 세개다 필수로 받아줬다!
  • Redirect URI : 카카오 로그인에서 code를 받을때 사용하는 리다이렉트 주소라고 보면된다. 프론트에서 받는거라서 3030으로 설정해주면 되고 난 테스트를 위해 8080까지 2개 등록해줬다.
    http://localhost:8080/kakao/callback
    http://localhost:3030/kakao/callback

내 애플리케이션에서 앱 키 > REST API 키redirect urienv파일 등에 넣어놓고 유출되지 않게 사용하기

다음은 build.gradle 설정


    //OAuth2
    implementation 'org.springframework.security:spring-security-oauth2-client'
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.3.5')
    implementation 'org.springframework.boot:spring-boot-starter-webflux'

    //JWT
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
    
	//Spring WebFlux
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    

OAuth2, JWT, webflux 설정을 추가해준다

Web Client 설정

카카오 api와의 통신을 위해 WebClientConfig를 추가

 @Configuration
public class WebClientConfig { // Spring WebFlux에서 HTTP 요청을 비동기로 처리하기 위한 WebClient를 설정

    // Netty HTTP 클라이언트 설정
    @Bean
    public ReactorResourceFactory resourceFactory() {
        ReactorResourceFactory factory = new ReactorResourceFactory();
        factory.setUseGlobalResources(false);
        return factory;
    }

    @Bean
    public WebClient webClient() {
        // HTTP 클라이언트 설정
        Function<HttpClient, HttpClient> mapper =
                client ->
                        HttpClient.create()
                                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) // 연결 시간 초과 1초로 설정
                                .doOnConnected(
                                        connection ->
                                                connection
                                                        .addHandlerLast(new ReadTimeoutHandler(10))
                                                        .addHandlerLast(new WriteTimeoutHandler(10))) // 읽기 및 쓰기 시간 초과 10초로 설정
                                .responseTimeout(Duration.ofSeconds(1)); // 응답 시간 초과 1초로 설정

        // HTTP 클라이언트와 연결
        ClientHttpConnector connector = new ReactorClientHttpConnector(resourceFactory(), mapper);

        // WebClient 생성
        return WebClient.builder().clientConnector(connector).build();
    }
}

https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증
해당블로그 참고했습니다

카카오 로그인 API 구현

1. Authorization Code 받아오기 (프론트)

https://kauth.kakao.com/oauth/authorize
?client_id=${REST_API_KEY}
&redirect_uri=${REDIRECT_URI}
&response_type=code

클라이언트에서 카카오 로그인 버튼을 누르면 (해당 주소로 요청을 보내면) 카카오 인증 서버로 요청이가고 Redirect URI에 code가 전달된다.

http://localhost:8080/kakao/callback?code={코드번호}

해당 코드번호를 서버로 넘겨주면 됨

2. Authorization Code를 통해 Access Token 받아오기 + Access Token을 통해 사용자 정보 가져오기

AuthController

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "Auth", description = "인증 API")
public class AuthController {

    private final KakaoAuthService kakaoAuthService;

    @Operation(summary = "카카오 로그인", description = "카카오 로그인을 진행합니다.")
    @GetMapping("/login/kakao")
    public ResponseEntity<ApiResponse<Object>> kakaoLogin(@RequestParam String code) {

        return ResponseEntity.status(HttpStatus.OK)
                .body(ApiResponse.from(kakaoAuthService.kakaoLogin(code)));
    }
}

kakaoAuthService

@RequiredArgsConstructor
@Service
@Slf4j
@Transactional(readOnly = true)
public class KakaoAuthService {

    private final KakaoApiClient kakaoApiClient;
    private final JwtTokenProvider jwtTokenProvider;
    private final UserService userService;

    @Transactional
    public String kakaoLogin(String code) {
        String accessToken =
                kakaoApiClient.getAccessToken(code); // 1. Authorization Code를 Access Token으로 교환

        Long userId = isSignedUp(accessToken); // 2. Access Token을 이용해 사용자 정보를 가져오고 없으면 회원가입

        HashMap<Long, String> map = new HashMap<>();
        map.put(userId, jwtTokenProvider.createToken(userId.toString())); // 3. JWT 토큰을 생성하여 반환한다.

        return map.get(userId);
    }

    @Transactional
    public Long isSignedUp(String token) {
        KakaoUserInfoResponse userInfo = kakaoApiClient.getUserInfo(token);
        return userService.findOrCreateUser(userInfo);
    }
}

KakaoApiClient

@RequiredArgsConstructor
@Slf4j
@Component
public class KakaoApiClient { // kakao API를 호출하기 위한 전용 class

    private final WebClient webClient;
    private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
    private static final String TOKEN_REQUEST_URI = "https://kauth.kakao.com/oauth/token";

    @Value("${KAKAO_API_KEY}")
    private String kakaoApiKey;

    @Value("${KAKAO_REDIRECT_URI}")
    private String kakaoRedirectUri;

    // 카카오 API 호출 : Authorization code -> Access Token
    public String getAccessToken(String code) {
        try {
            KakaoTokenResponse response =
                    webClient
                            .post()
                            .uri(TOKEN_REQUEST_URI)
                            .header("Content-Type", "application/x-www-form-urlencoded")
                            .bodyValue(
                                    "grant_type=authorization_code&client_id="
                                            + kakaoApiKey
                                            + "&redirect_uri="
                                            + kakaoRedirectUri
                                            + "&code="
                                            + code)
                            .retrieve()
                            .bodyToMono(KakaoTokenResponse.class)
                            .block();

            return response.accessToken();

        } catch (WebClientResponseException e) {
            throw new RuntimeException("Failed to fetch access token from Kakao.", e);
        }
    }

    // 카카오 API 호출 : Access Token -> 사용자 정보 조회
    public KakaoUserInfoResponse getUserInfo(String token) {
        try {
            return webClient
                    .get()
                    .uri(USER_INFO_URI)
                    .header("Authorization", "Bearer " + token)
                    .retrieve()
                    .bodyToMono(KakaoUserInfoResponse.class)
                    .block();
        } catch (WebClientResponseException e) {
            throw new RuntimeException("Failed to fetch access token from Kakao.", e);
        }
    }
}

여기가 카카오 api와 통신하는 전부라고 보면되는데 먼저 코드를 토큰으로 교환해주기 위해서 getAccessToken을 호출한다. oauth/token 으로 apikey, redirect uri, code를 보내고 토큰을 받으면 된다. 여기까지 하면 카카오 서버에 회원이 등록된 것이라고 보면된다.

토큰을 받으면 이 토큰을 통해 카카오 api에서 사용자 정보를 조회할 수 있다.
getUserInfo 메소드를 통해 먼저 카카오 api에서 정보를 가져오고 이 정보가 데이터베이스에 저장되어있지 않은경우(신규 유저의 경우)에는 리포지토리에 저장해준다. 이부분은 userService에 자유롭게 작성하면된다.

이를 위해 작성해준 dto들이다.

public record KakaoAccount(
        Boolean profile_needs_agreement,
        Boolean email_needs_agreement,
        KakaoProfile profile,
        String email
) {}

public record KakaoProfile(
        String nickname,
        String profile_image_url // 프로필 이미지 URL
) {}

public record KakaoProperties(
		String profile_image, 
        String thumbnail_image
) {}

public record KakaoTokenResponse(
        @JsonProperty("access_token") String accessToken,
        @JsonProperty("token_type") String tokenType,
        @JsonProperty("expires_in") int expiresIn,
        @JsonProperty("refresh_token") String refreshToken,
        @JsonProperty("scope") String scope) {}

public record KakaoUserInfoResponse(
        Long id, 
        KakaoProperties properties,
        KakaoAccount kakao_account) {}

kakao developers 에서 설정했던 것들 토대로 필요한것들만 받아와주면 된다.
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
REST API > 사용자 정보 가져오기 에서 properties, profile에 대해 자세히 볼 수 있다.

JWT토큰을 생성해서 반환하기

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    @Value("${spring.jwt.secret}")
    private String secretKey;

    @Value("${spring.jwt.access.token.expiration}")
    private long accessTokenExpiration;

    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userId) {

        Claims claims = Jwts.claims().setSubject(userId); // JWT payload 에 저장되는 정보단위
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + accessTokenExpiration)) // 토큰 유효시간 설정 (5시간)
                .signWith(SignatureAlgorithm.HS512, secretKey) // 암호화 알고리즘, 시크릿 키 설정
                .compact();
    }

jwt secret key와 만료시간은 env 파일에 저장해주었다.
jwt secret key는 아무거로나 해도 되는데 HS512 알고리즘 사용을 위해서는 최소 512비트 이상으로 설정해줘야한다.

여기까지하면 카카오 로그인 + jwt 토큰 완료!
swagger를 통해 확인해보겠다코드를 넣고 excute를 하면 아래와 같은 jwt 토큰을 발급해준다이 토큰을 Authorize에 넣고 로그인해주면 인증완료

그러나 토큰을 제대로 사용하려면 모든 요청에 포함시켜야하고 유효성 검사하는 로직등이 필요하다.
다음에는 자동 필터링 과정과 컨트롤러에서 사용할수있는 예시에 대해 알아보자.

📌 고민되는것들

  • 웹기준으로 작성했는데 안드로이드 환경에서도 잘 돌아갈까?
  • code를 프론트에서 넘겨주는 절차가 잘 작동할지?

참고 블로그 🙇
https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증
https://velog.io/@jiwoow00/Spring-boot-JWT-이용한-백엔드-카카오-로그인

profile
💻 개발 공부 기록장

0개의 댓글

관련 채용 정보