Google, Github 로그인 구현 이후 카카오톡 로그인을 구현해보도록 하겠습니다.
카카오 로그인은 다음과 같은 로직으로 진행됩니다.
또한 카카오 로그인은 CommonOAuth2Provider에 따로 등록되어있지 않아서 따로 작업을 해주어야 합니다.
값 : http://localhost:8080/login/oauth2/code/kakao
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
이렇게 하면 사전 작업은 끝났습니다!
서론에 넣어놨던 사진을 다시 보며 step별로 진행해보도록 하겠습니다.
사용자가 카카오 로그인 버튼을 클릭하면 인가 코드를 받기 위한 URL로 리디렉션됩니다. 이 과정은 클라이언트(Vue.js)와 Spring Boot 백엔드 간의 상호작용으로 시작됩니다.
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;
}
}
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;
}
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);
}
}
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();
}
}
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));
}
}
주요 로직에 포함되어 있지 않지만 기능 구현에 필요한 기타 클래스를 정리해보았습니다.
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();
}
}
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;
}
}
}
}
지금까지 한 클래스들을 정리해보겠습니다.
일단 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 <- 여기를 참고하시면 되겠습니다!