카카오 로그인 시도 (2)
코드를,, 블로그를,, 몇 번 지웠다가 썼다가를 반복한지 모르겠다,,,,
소셜 로그인 중 카카오 로그인이 제일 쉽다고 들었는데,,,, ㅠㅠㅠㅠㅠㅠㅠ
관련 블로그와 카카오 문서를 계속 본 것 같다. 이제 어떻게 돌아가는지 정확히 안다. 성공만 하면 된다!!
이 블로그가 최종이길 바라며 다시 처음부터 차근차근 해보려고 한다.
먼저 카카오 문서다.
https://developers.kakao.com/docs/latest/ko/kakaologin/common
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
카카오 로그인 과정
1. [카카오톡 로그인] 버튼(LoginJoinButton(mode="kakao"))을 누르면 KakoAuthUrl로 이동한다.
2. KakaoAuthUrl에서 카카오 로그인이 완료되면, RedirectUri로 이동한다.
3. RedirectUri URL에 뜨는 인가코드를 프론트에서 추출한다.
4. 이 인가코드를 API에 쿼리스트링으로 같이 파싱하여 백엔드로 넘겨준다.
5. 백엔드에서는 이 인가코드를 받아서 토큰을 받아 프론트로 넘겨준다.
6. 토큰을 받아 로그인을 유지한다.
내 애플리케이션 설정
먼저, 위에 Kako Developers에 들어가서 애플리케이션을 등록해야 한다.
나는 테스트용으로 추가하였다.
앱 키를 확인할 수 있다.
우리는 REST API 키를 사용할 것이다.
플랫폼을 설정해준다.
활설화 설정 상태를 ON으로 바꾸고 Redirect URI를 설정한다.
동의 항목도 체크해준다.
우리 프로젝트에서는 커뮤니티 이용을 위해 이름(name)이 필요하고,
아이디 찾기를 위해 전화번호(phone_number)가 필요하고, 임시 비밀번호 발급을 위해 이메일(account_email)이 필요하다.
React 구현
앱 키는 무조건 .env에서 관리하고 꼭 gitignore에 추가시켜야 한다.
[앱 키]에 있는 REST API 키와 위에 Client Secret 코드를 .evn에 적으면 된다.
1) 로그인 화면 페이지와 2) Redirect URI로 갈 때 보여질 화면 페이지를 생성한다.
1) 로그인 화면 페이지는 이미 만들어둔 <LoginJoinButton> 컴포넌트를 사용할 것이다.
src/common/button/components/LoginJoinButton.jsx
const REST_API_KEY = process.env.REACT_APP_REST_API_KEY;
const REDIRECT_URI = `http://localhost:3000/oauth`;
const kakaoURL = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;
...
) : isKakaoMode ? (
<div className="ljb_set">
<div onClick={() => window.location.href = kakaoURL}>
<img src={kakao} alt="카카오톡 로그인" />
</div>
<span>카카오톡 로그인</span>
</div>
) : isGoogleMode ? (
...
src/App.js
import Oauth from "./login/components/Oauth";
...
function App() {
return (
<Router>
<Routes>
...
<Route path="/oauth" element={<Oauth />} />
...
</Routes>
</Router>
);
}
export default App;
로그인을 하면 인가코드가 뜬다.
src/login/Oauth.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
export function Oauth() {
const navigate = useNavigate();
const [isFetching, setIsFetching] = useState(false); // 상태 변수 추가
const REST_API_KEY = process.env.REACT_APP_REST_API_KEY;
const REDIRECT_URI = `http://localhost:3000/oauth`;
const code = new URL(window.location.href).searchParams.get("code");
const getCode = async () => {
setIsFetching(true); // 요청을 시작할 때 상태를 true로 설정
try {
const response = await axios.post('https://kauth.kakao.com/oauth/token',
{
grant_type: "authorization_code",
client_id: REST_API_KEY,
redirect_uri: REDIRECT_URI,
code: code,
},
{
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
},
}
);
return response;
} finally {
setIsFetching(false); // 요청이 완료된 후 상태를 false로 설정
}
};
useEffect(() => {
if (!isFetching) { // 이미 요청 중이지 않은 경우에만 실행
getCode()
.then((response) => {
if (response) {
localStorage.setItem("code", JSON.stringify(response.data.access_token));
navigate("/");
}
})
.catch((err) => console.log(err));
}
}, [isFetching]); // 상태 변수를 의존성 배열에 추가
return <></>;
};
export default Oauth;
응답으로 받은 scope 안의 값들을 보면 내가 동의항목으로 표시한 account_email, name, phone_number가 찍힌 걸 알 수 있다.
하지만 이거와 별개로 access_token을 통해 추가 요청을 보내 사용자 정보를 가져와야 한다.
카카오의 access_token 만으로는 직접 사용자 정보를 제공하지 않기 때문이다.
Oauth.jsx의 useEffect()를 수정하면
useEffect(() => {
if (!isFetching) {
getCode()
.then((response) => {
if (response) {
localStorage.setItem("access_token", JSON.stringify(response.data.access_token));
axios.post("http://localhost:8080/api/public/kakao", {
accessToken: response.data.access_token
})
.then(res => {
navigate("/"); // 로그인 후 리다이렉트
})
.catch(err => console.log(err));
}
})
.catch((err) => console.log(err));
}
}, [isFetching]);
access_token을 로컬 스토리지에 저장하고, 이 후에 access_token을 이용해 백엔드 서버에 요청을 보내 사용자 정보를 가져올 수 있도록 해야한다.
Spring Boot 구현
먼저 DTO를 생성해주었다.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakoInfoDTO {
private Long id;
private String name;
private String account_email;
private String phone_number;
}
서비스 코드는 두 개의 메소드가 필요하다.
public interface UserService {
// 카카오로 회원 정보 가져오기
KakoInfoDTO getUserInfoFromKakao(String accessToken);
// 카카오 로그인
User kakaoLogin(KakoInfoDTO kakoInfoDTO);
}
@Override
public KakoInfoDTO getUserInfoFromKakao(String accessToken) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
HttpEntity<String> entity = new HttpEntity<>("", headers);
ResponseEntity<String> response;
try {
response = restTemplate.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
entity,
String.class
);
} catch (HttpClientErrorException e) {
throw new RuntimeException("카카오 API 호출에 실패했습니다: " + e.getMessage());
}
if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("카카오 API 응답 상태가 좋지 않습니다: " + response.getStatusCode());
}
// 응답을 로그에 출력
log.info("카카오 API 응답: {}", response.getBody());
// JSON 응답을 DTO로 매핑
ObjectMapper objectMapper = new ObjectMapper();
KakoInfoDTO userInfo;
try {
userInfo = objectMapper.readValue(response.getBody(), KakoInfoDTO.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("카카오 API 응답을 처리하는 중 오류가 발생했습니다: " + e.getMessage());
}
return userInfo;
}
@Override
public User kakaoLogin(KakoInfoDTO kakoInfoDTO) {
User user = User.builder()
.userName(kakoInfoDTO.getName())
.email(kakoInfoDTO.getAccount_email())
.password("kakao_password")
.phone(kakoInfoDTO.getPhone_number())
.role(Role.USER)
.userState(UserState.ACTIVE)
.kakaoId(kakoInfoDTO.getId())
.build();
return userRepository.save(user);
}
컨트롤러
@PostMapping("api/public/kakao")
public ResponseEntity<User> kakaoLogin(@RequestBody Map<String, String> payload) {
String accessToken = payload.get("accessToken");
KakoInfoDTO kakoInfoDTO = userService.getUserInfoFromKakao(accessToken);
User user = userService.kakaoLogin(kakoInfoDTO);
return ResponseEntity.ok(user);
}
Spring Security의 OAuth2 클라이언트 설정에서 발생하는 것으로, ClientRegistrationRepository 빈이 없어서 발생한 문제이다. 이 빈은 OAuth2 클라이언트 설정을 위해 필요한 등록 정보를 관리한다.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' #1)
implementation 'org.springframework.boot:spring-boot-starter-web' #2)
}
#1) OAuth 2.0 클라이언트 기능을 추가한다. 이를 통해 애플리케이션은 OAuth 2.0 인증을 사용하는 다양한 외부 서비스와의 연동을 쉽게 구현할 수 있다.
#2) JSON/XML 등의 데이터 직렬화 및 역직렬화 지원한다.
application.properties
spring.security.oauth2.client.registration.kakao.client-id=REST API 키
spring.security.oauth2.client.registration.kakao.redirect-uri=REDIRECT URI
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=name,account_email,phone_number
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
[refactor 필요]
WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
...
.allowedHeaders("*")
}
}
.allowedHeaders("*")
CORS 설정을 할 때 사용되는 메소드이다. 이 메소드는 클라이언트가 서버로 요청을 보낼 때, 어떤 HTTP 헤더들을 허용할지를 설정하는 데 사용된다. 모든 헤더를 허용하겠다는 의미이다.
아직 개발 단계이므로 빠르게 개발하고 테스트하기 위해 모든 헤더를 허용했지만 리팩토링 단계에서는 필요한 헤더만 허용해야 한다.
SecurityConfig.java
...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll() // 공용 엔드포인트
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 관리자 엔드포인트
.requestMatchers("/api/user/**").hasRole("USER") // 사용자 엔드포인트
.anyRequest().authenticated() // 그 외 모든 요청은 인증 필요
)
// OAuth2를 통해 사용자 인증 처리
.oauth2Login(oauth2 -> oauth2
.loginPage("/oauth") // 로그인 페이지 경로
.defaultSuccessUrl("/", true) // 로그인 성공 시 리디렉션될 URL
.failureUrl("/oauth?error=true") // 로그인 실패 시 리디렉션될 URL
.userInfoEndpoint(userInfo -> userInfo
.userService(oauth2UserService())) // 사용자 정보 oauth2UserService() 메소드로 처리
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return request -> {
OAuth2User oAuth2User = delegate.loadUser(request);
Map<String, Object> attributes = oAuth2User.getAttributes(); // attributes 맵에서 get
String kakaoId = String.valueOf(attributes.get("id")); // 카카오 사용자 ID
// kakaoAccount : 추가 정보가 포함된 객체(이메일, 이름, 전화번호)
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
String email = (String) kakaoAccount.get("email");
String userName = (String) kakaoAccount.get("name");
String phone = (String) kakaoAccount.get("phone_number");
return oAuth2User;
};
}
...
ㄴㄴ