지난 번에 DefaultOAuth2Service 를 상속하여
스프링 시큐리티에 등록하는 방식으로 카카오 로그인을 구현해보았다
하지만 이런 식으로 하게 되면
스프링 내장 함수을 이용하면서 편리하게 구현을 할 수는 있지만
구조가 너무 추상적이고 컨트롤러에서 통제하기가 어려웠다
그래서 이번에는 OAuth 인증 과정의 본질을 뜯어보면서 하나하나 구현해보았다
💾 이전 글 (DefaultOAuth2Service 이용)
https://gengminy.tistory.com/39
🚀 Kakao Developers 설정
시작하기
어플리케이션 추가하기 누르고 앱 이름과 사업자명 입력
이후 만든 어플리케이션 클릭해서 이동
REST API 키 나중에 사용해야 하니까 이 페이지 기억해두기
좌측 메뉴 카카오 로그인 -> 활성화 설정 상태 ON 으로 변경
이후 아래 Redirect URI 설정으로 진입
일단 로컬 환경에서 테스트를 진행해야 하기 때문에 로컬호스트로 설정
리액트에서 localhost:3000 사용하니까 그 쪽으로 등록해준다
이전 글에서는 localhost:8000 사용해서 서버 포트로 들어갔는데
이번에는 리액트랑도 완벽하게 연결하기 위해 바꾸었다
로그인 시도할 때 이 URI 를 통해 인가 코드가 반환이 된다
이 URI 은 클라이언트 쪽에서 사용해야 하니까 잘 기억해두기
카카오 로그인 -> 동의항목 에서
OAuth 를 통한 회원가입 진행 시 받아올 정보를 설정
🚀 인증 구조
클라이언트 <-> 백엔드 서버 <-> 카카오 api 서버가
수 차례 통신하면서 로그인 과정을 진행하게 된다
본질은 같아서 하나만 구현해보면 나머지는 쉽게 할 수 있을 거 같다
🔨 프론트엔드 (React)
📝 KakaoLogin.js
const KakaoLogin = () => {
const location = useLocation();
const param = useParams();
const KAKAO_BASEURL = process.env.REACT_APP_KAKAO_BASEURL;
const KAKAO_RESTAPI_KEY = process.env.REACT_APP_KAKAO_RESTAPI_KEY;
const KAKAO_REDIRECT_URI = process.env.REACT_APP_KAKAO_REDIRECT_URI;
const loginUri =
'https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=' +
KAKAO_RESTAPI_KEY +
'&redirect_uri=' +
KAKAO_BASEURL +
KAKAO_REDIRECT_URI;
return (
<>
<NavBar />
<h1>카카오 로그인</h1>
<a href={loginUri}>카카오 로그인</a>
</>
);
};
export default KakaoLogin;
아까 카카오 디벨로퍼에서 설정했던 정보를
dotenv 파일에 다 담아두고 사용 중이다
이 정보를 이용해서 카카오 로그인 요청 시 해당 url로 넘어가게 하면 된다
해당 url 로 넘어가면 유저에게 카카오 계정 로그인을 요청하고
카카오 계정 로그인이 완료되면 다음 스텝으로 넘어간다
📝 KakaoProcess.js
const KakaoProcess = () => {
const params = new URL(document.location).searchParams;
const code = params.get('code'); //쿼리파라미터 code 가져오기
const setAccessToken = useCookies(['accessToken'])[1];
useEffect(() => {
async function dispatchLogin() {
try {
AuthApi.requestKakaoLogin(code).then(res => {
if (res.status === 200) {
const date = new Date();
const { accessToken } = res.data.data;
setAccessToken('accessToken', accessToken, {
expires: new Date(date.setDate(date.getDate() + 3)),
path: '/',
secure: true,
});
console.log(res.data.data);
}
});
} catch (error) {
console.error(error);
}
}
dispatchLogin();
}, []);
return (
<>
<NavBar />
<h1>로그인 처리중입니다</h1>
</>
);
};
export default KakaoProcess;
카카오 계정 로그인이 완료되면 query parameter 로
해당 유저의 계정 코드가 같이 넘어오는데
이를 캐치해서 백엔드 서버에 axios post 하는 방식으로 진행했다
아까 설정한 redirect url + code 가 넘어온다
📝 AuthApi.js
const AuthApi = {
...
...
requestKakaoLogin: async body => {
const data = await axios.post(
'http://localhost:8000/auth/login/kakao',
body,
{ headers: { 'Content-Type': 'application/json' } }
);
return data;
},
};
export default AuthApi;
일단 테스트 중이니 localhost:8000으로 날리는 방식
서버에서는 /auth/login/kakao 를 컨트롤러에서 받아서 처리한다
간혹 content-type 이 urlencoded 방식이라고 안되는 경우가 있는데
헤더에 application/json 방식을 정의해주면 해결된다
이제 백엔드 서버 로직을 보자
🔨 백엔드 (Spring)
📝 KakaoController.java
@Slf4j
@Controller
@RequestMapping("/auth/login")
@RequiredArgsConstructor
public class KakaoController {
private final KakaoService kakaoService;
private final AuthService authService;
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
@ApiOperation(value = "카카오 코드를 포함한 로그인 요청")
@PostMapping(value = "/kakao", produces = "application/json; charset=utf-8")
@ResponseBody
public CommonApiResponse<TokenResponseDto> postKakaoLoginWithCode(
@RequestBody KakaoRequest kakaoRequest,
HttpServletResponse response
) {
KakaoTokenDto kakaoTokenDto = kakaoService.getKakaoAccessToken(kakaoRequest.getCode());
KakaoUserDto kakaoUserDto = kakaoService.getKakaoUser(kakaoTokenDto.getAccessToken());
TokenResponseDto tokenResponseDto = authService.loginKakao(kakaoUserDto);
return CommonApiResponse.of(tokenResponseDto);
}
}
처리하는 엔드포인트는 /auth/login/kakao
KakaoRequest 라는 Dto를 받아서 처리한다
별건 없고 String code 를 가지고 있는 Dto 이다
나머지 로직은 서비스 단에서 구현했다
반환값은 JWT 관련 Dto 인데
엑세스 토큰과 리프레시 토큰을 반환한다
📝 KakaoRequest.java
@Getter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KakaoRequest {
private String code;
}
입력받는 dto 정의
프론트의 유저 code 를 가지고 있다
📝 KakaoService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = false)
@Slf4j
public class KakaoService {
private final WebClient webClient;
private final UserRepository userRepository;
@Value("${KAKAO_BASEURL}")
private String KAKAO_BASEURL;
@Value("${KAKAO_RESTAPI_KEY}")
private String KAKAO_RESTPAPI_KEY;
@Value("${KAKAO_REDIRECT_URI}")
private String KAKAO_REDIRECT_URL;
public KakaoTokenDto getKakaoAccessToken(String code) {
String getTokenURL =
"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id="
+ KAKAO_RESTPAPI_KEY + "&redirect_uri=" + KAKAO_BASEURL + KAKAO_REDIRECT_URL + "&code="
+ code;
try {
KakaoTokenDto kakaoTokenDto =
webClient.post()
.uri(getTokenURL)
.retrieve()
.bodyToMono(KakaoTokenDto.class).block();
return kakaoTokenDto;
} catch (Exception e) {
e.printStackTrace();
throw new BadRequestException(ErrorCode.KAKAO_BAD_REQUEST);
}
}
public KakaoUserDto getKakaoUser(String kakaoAccessToken) {
String getUserURL = "https://kapi.kakao.com/v2/user/me";
try {
KakaoUserDto kakaoUserDto =
webClient.post()
.uri(getUserURL)
.header("Authorization", "Bearer " + kakaoAccessToken)
.retrieve()
.bodyToMono(KakaoUserDto.class)
.block();
return kakaoUserDto;
} catch (Exception e) {
e.printStackTrace();
throw new BadRequestException(ErrorCode.KAKAO_BAD_REQUEST);
}
}
}
getKakaoAccessToken 에서는
카카오 유저 코드를 이용해 카카오 계정 관련 권한이 담긴 엑세스 토큰을 가져온다
처음할 때 혼동이 있을 수 있는데
이 엑세스 토큰은 카카오 api 서버에서 카카오 계정 관련 서비스를 이용하기 위함이지
우리가 만드는 서비스 관련 엑세스 토큰이 아니다
여기서는 서버에서도 프론트의 axios 와 같이
서버에서 카카오 api 서버와 통신하는 웹 클라이언트가 필요하다
이 때 사용하는게 RestTemplete 과 WebClient 인데
RestTemplete 은 지원이 끊기니 사용하지 않는게 좋다고 들어서
Webflux의 WebClient 를 사용했다
📝 WebClientConfig.java
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofMillis(5000))
.doOnConnected(connection -> {
connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS));
});
WebClient webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.APPLICATION_JSON))
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
httpClient.warmup().block();
return webClient;
}
}
웹 클라이언트를 싱글톤 빈으로 사용하기 위한 설정 파일이다
여러가지 WebClient 생성 관련 설정을 해줄 수 있다
의존성은 다음을 build.gradle 에 추가해주면 된다
implementation 'org.springframework.boot:spring-boot-starter-webflux'
처음에 얻는 엑세스 토큰으로
다시 카카오 api 서버에 해당 유저를 가져오는 요청을 보낼 수 있다
이를 kakaoUserDto 에 저장했다
📝 KakaoUserDto.java
@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class KakaoUserDto {
@JsonProperty("id")
private String authenticationCode;
@JsonProperty("connected_at")
private Timestamp connectedAt;
@JsonProperty("kakao_account")
private KakaoAccount kakaoAccount;
@JsonProperty("properties")
private Properties properties;
@Getter
@ToString
public static class KakaoAccount {
private String email;
}
@Getter
@ToString
public static class Properties {
private String nickname;
}
}
카카오 계정 로그인한 유저가 정보 제공에 동의했다면
그 정보들이 다 넘어와 각 필드에 저장된다
📝 AuthService.java
...
...
@Override
@Transactional
public TokenResponseDto loginKakao(KakaoUserDto kakaoUserDto) {
String provider = "kakao";
log.info("로그인 시도");
Optional<User> user = userRepository.findByEmailAndProvider(
kakaoUserDto.getKakaoAccount().getEmail(),
provider
);
if (user.isPresent()) {
log.info("가입된 회원");
/* 이미 가입된 회원 */
TokenResponseDto tokenResponseDto = jwtTokenProvider.generateSocialToken(user.get());
return tokenResponseDto;
} else {
/* 새로 가입할 회원 */
String email = kakaoUserDto.getKakaoAccount().getEmail();
String name = kakaoUserDto.getProperties().getNickname();
User newUser = User.builder()
.email(email)
.name(name)
.role(UserRole.ROLE_USER)
.provider(provider)
.authenticationCode(kakaoUserDto.getAuthenticationCode())
.build();
userRepository.save(newUser);
log.info("새로운 회원");
TokenResponseDto tokenResponseDto = jwtTokenProvider.generateSocialToken(newUser);
return tokenResponseDto;
}
}
...
...
이제 이전에 만든 로그인 관련 서비스인 AuthService 에 일부 로직을 추가해준다
KakaoUserDto 를 받아서 해당 유저의 소셜 로그인을 처리한다
만약 DB 상 유저가 있다면 로그인을 시키고
그렇지 않다면 회원가입 시킨다
이후 해당 유저의 정보가 담긴 JWT를 생성해서 리턴해주면 된다
이제 프론트에서 요청을 보내면
이런식으로 엑세스 토큰과 리프레시 토큰이 잘 넘어오게 된다
이를 리다이렉션 시켜서 다시 서비스를 시작하면 된다
OAuth 구조 이해하기가 힘들어서 일단 구현하면서 부딪혀봤는데 꽤 어려웠다
아무래도 너무 추상화된 코드 보다는 이런식으로 하나하나 뜯어서 하는게 성에 차는 거 같다
이제 다른 소셜 로그인도 구현해 봐야지
'📡 백엔드 > 🌱 Spring Boot' 카테고리의 다른 글
[Spring] 스프링 애플 로그인 구현하기 (Sign in with Apple OIDC) (1) | 2023.03.25 |
---|---|
[Spring] 스프링 Feign Client 적용하기 (Spring Cloud OpenFeign) (1) | 2023.03.25 |
[Spring] OAuth2Service + 스프링 시큐리티 + JWT로 카카오 로그인 구현하기 (0) | 2022.08.27 |
[Spring] Swagger 연동 시 Unable to infer base url 접속 에러 (ResponseBodyAdvice) (0) | 2022.08.11 |
[Spring] 스프링 시큐리티로 CORS와 preflight 설정하기 (0) | 2022.08.11 |