이번 글에서는 카카오 api를 이용한 회원가입, 로그인 + jwt 토큰 발급 과정에 대해 정리해보겠습니다.
위의 그림이 카카오 공식 문서에 나와있는 서비스 로그인 과정으로
글로 다시 적어보자면 아래와 같습니다
- 클라이언트가 카카오 로그인 요청
- 클라이언트는 카카오로부터 code를 받아 서버로 전달
- code를 통해 서버에서 카카오로 토큰 발급 요청
- 카카오는 code 등을 검증 후 토큰을 서버로 전달
- 서버에서 토큰으로 유저 정보 조회, 등록
- 서버에서 유저에게 JWT 토큰을 생성 및 전달
- 유저는 요청마다 jwt토큰을 포함해서 서버에 요청
차근차근..해봅시다
kakaodevelopers에 들어가서 내 애플리케이션 > 애플리케이션 추가하기
이름이랑 아이콘 이런거 설정해서 하나 만들어준다.생성한 애플리케이션으로 들어가서 아래 항목들을 설정해줘야한다.
내 애플리케이션에서 앱 키 > REST API 키와 redirect uri는 env
파일 등에 넣어놓고 유출되지 않게 사용하기
다음은 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 설정을 추가해준다
카카오 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-검증
해당블로그 참고했습니다
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
에 대해 자세히 볼 수 있다.
@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에 넣고 로그인해주면 인증완료
그러나 토큰을 제대로 사용하려면 모든 요청에 포함시켜야하고 유효성 검사하는 로직등이 필요하다.
다음에는 자동 필터링 과정과 컨트롤러에서 사용할수있는 예시에 대해 알아보자.
📌 고민되는것들
참고 블로그 🙇
https://velog.io/@win-luck/Springboot-카카오-소셜로그인-Jwt-토큰-발급-및-API-검증
https://velog.io/@jiwoow00/Spring-boot-JWT-이용한-백엔드-카카오-로그인