API

스프링부트 카카오 로그인 버튼 구현하기 [REST API/후기]

rexondex 2024. 10. 21. 16:38

현재 프로젝트 구조

 

카카오 API 문서에 따라 로그인버튼을 구현해보겠습니다.

 

1) index.html 을 작성합니다.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>index</title>
</head>
<body>
<h2>index.html</h2>
<button onclick="location.href='/login/kakao'">
    카카오 로그인
</button>
</body>
</html>

 

 

2)  로그인 이후 리다이렉트 될 home.html 을 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>home</title>
</head>
<body>
<h2>home.html</h2>
<h2>로그인 성공.</h2>
</body>
</html>

 

 

3) KakaoController 를 작성합니다.

@Controller
public class KakaoController {

	// 어플리케이션 프로퍼티스에서 값을 가져옴
    @Value("${KAKAO_CLIENT_ID}")
    private String KAKAO_CLIENT_ID;

	// 어플리케이션 프로퍼티스에서 값을 가져옴
    @Value("${KAKAO_SERVER_REDIRECT_URI}")
    private String KAKAO_SERVER_REDIRECT_URI;

    @GetMapping("/login/kakao")
    public void kakaoLoginRedirect(HttpServletResponse response) throws IOException {
        String clientId = KAKAO_CLIENT_ID; // 카카오 API 클라이언트 ID
        String redirectUri = KAKAO_SERVER_REDIRECT_URI; // 설정한 리디렉션 URI

        String url = "https://kauth.kakao.com/oauth/authorize?client_id=" + clientId
                + "&redirect_uri=" + redirectUri + "&response_type=code";

        response.sendRedirect(url); // 카카오로 리다이렉트
    }

    @GetMapping("/login/kakao/callback")
    public String kakaoCallback(@RequestParam("code") String code) throws JsonProcessingException {
        String clientId = KAKAO_CLIENT_ID;
        String redirectUri = KAKAO_SERVER_REDIRECT_URI;
        String tokenUrl = "https://kauth.kakao.com/oauth/token";

        // 파라미터 설정
        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", code);

        // RestTemplate으로 POST 요청
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

        // 카카오로 액세스 토큰 요청
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.postForEntity(tokenUrl, request, String.class);
            if (response.getStatusCode() != HttpStatus.OK) {
                throw new RuntimeException("Failed to retrieve access token");
            }
        } catch (RestClientException e) {
            // 에러 처리 로직
            return "error";  // 에러 페이지로 리디렉션 또는 로그 기록
        }

        // JSON 파싱 후 액세스 토큰 추출 (Jackson 라이브러리 사용)
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(response.getBody());
        String accessToken = jsonNode.get("access_token") != null ? jsonNode.get("access_token").asText() : null;
        if (accessToken == null) {
            throw new RuntimeException("Access token not found in response");
        }

        // 액세스 토큰을 활용한 로직 구현 필요
        return "redirect:/home";
    }
}

 

 

컨트롤러에서 @Value("${KAKAO_CLIENT_ID}") 로 값을 가져오고 있으므로

application.properties에 이렇게 추가해 두었습니다.

저는 리다이렉트 주소를 /login/kakao/callback 으로 지정해뒀습니다.

디벨로퍼 페이지의 REDIRECT URI와 동일하게 설정되어야 합니다. (로컬환경에서 http://)

# REST API KEY
KAKAO_CLIENT_ID=카카오 디벨로퍼의 발급받은 REST API 키 값
# 카카오 디벨로퍼 페이지의 REDIRECT URI와 동일하게 설정
KAKAO_SERVER_REDIRECT_URI=http://localhost:8080/login/kakao/callback

카카오 디벨로퍼 리다이렉트 URI
컨트롤러 매핑
프로퍼티스

이렇게 모두 일치되어야 합니다!

 

 

4) template 하위에 있는 html 리소스들을 호출하는 HomeController를 작성합니다.

@Controller
public class HomeController {

    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/home")
    public String home() {
        return "home";
    }

    /* 에러페이지 매핑 필요한경우
    @GetMapping("/error")
    public String error() {
        return "error";
    }
    */

}

 

* 5) OAuth2와 함께 Spring Security를 적용했다면 다음 클래스도 추가합니다 *

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf
                        .disable()  // CSRF 보호 비활성화 (필요한 경우 활성화)
                )
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/index").permitAll()  // 경로는 인증 없이 접근 허용
                                .anyRequest().authenticated()  // 그 외 모든 요청은 인증 필요
                )
                .formLogin(withDefaults())  // 기본 로그인 폼 활성화
                .logout(withDefaults());  // 기본 로그아웃 활성화

        return http.build();
    }
}

 

localhost:8080에 접근했을 때 카카오 로그인을 호출하는 버튼이 있는 페이지 "index.html"으로의 접근을 제외하고

나머지 모든 경로에 대해 인증을 요구합니다.

 

// application.properties 에 작성해 두면 임시 시큐리티 로그인 정보가 됩니다.
spring.security.user.name=admin
spring.security.user.password=admin123

 

 

 

6) 브라우저에서 localhost:8080 에 접속해봅니다.

'/' 는 '/index'가 아니기때문에 시큐리티 로그인이 뜹니다

 

'localhost:8080/' 에서는 로그인 창이 뜨고 'localhost:8080/index'로 정확히 입력해야 로그인절차를 무시합니다.

'/' 주소에서 지금 서버는 이미 index.html 을 호출하도록 되어있지만 매핑되지 않은 경로이므로 '/login' 이 먼저 호출되었습니다. 로그인 후에 '/index'를 호출하여 index.html을 불러올 것입니다.

 

'/'도 .requestMatcher() 에 추가해두면 'localhost:8080/' 에서도 로그인절차를 무시할 수 있습니다.

모든 로그인절차를 무시하려면 .requestMatcher("/**").permitAll() 로 변경하면 됩니다.

// "/"와 "/index" 둘다 허용
.requestMatchers("/", "/index").permitAll()

// 루트 디렉토리의 바로 아래 경로만 허용 ( /example/test 는 허용하지 않음 )
.requestMatchers("/*").permitAll()

// 모든 경로(하위경로 포함) 허용
.requestMatchers("/**").permitAll()

 

localhost:8080/index 에서 로그인 버튼 확인

 

7) 로그인 버튼을 클릭해서 로그인 진행합니다.

카카오 로그인 화면

 

 

8) 로그인이 성공했다면 "/home"으로 이동합니다.

 

* 9) 로그인이 실패했을 경우에는 "/error"를 호출하도록 컨트롤러에 기입해놓았습니다. *

[처음으로 돌아가기]를 누르면 /index로 이동합니다.

 

/* 
	error.html 예시입니다.
*/

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>error</title>
</head>
<body>
<h2>로그인 과정에서 에러가 발생했습니다.</h2>
<button onclick="location.href='/index'">처음으로 돌아가기</button>
</body>
</html>

 

 


 

여기까지 카카오 로그인 버튼을 스프링부트 OAuth2를 통해 간단하게 구현해보았습니다.

카카오 로그인을 하면 사용자 로그인 정보를 사용자허락을 받고 파라미터로 받아올 수도 있습니다.

저는 이 부분은 현재 생략했습니다.

 

그리고 이렇게 작동이 된다는 것만 알게되었을 뿐 실제로는 신경쓸게 더 많았습니다.

토큰을 저장하고 안전하게 관리하는 법에 대해서 더 많은 고민이 필요할 것입니다.

 

 

// KakaoController 클래스에서...

        // 액세스 토큰을 활용한 로직 구현 필요
        return "redirect:/home";

 

라고 되어있는 부분이 있는데

일단 (1)액세스 토큰을 받았으면 로그인이 완료된 것입니다. (2)그래서 곧바로 return문을 통해 home으로 리다이렉트합니다.

 

이때 액세스 토큰을 활용한 추가 로직을 작성한다면

(1)액세스 토큰을 받고, (2)이를 서버에서 안전하게 관리하기 위해 추가 로직을 구현합니다. (3)그리고 return문을 통해 home으로 리다이렉트하게 됩니다.

 

 


 

이 프로젝트를 진행하면서 느낀 점)

 

프론트엔드 쪽에서 바로 API를 요청해 데이터를 받아올 수 있지만 서버가 필요한 이유는

API키, 보안키, 시크릿 키 등을 프론트쪽에서 처리하게 되면 정보가 드러나 위험하기 때문이라고 생각했습니다.

 

그래서 프론트엔드에서 API를 서버로 요청하고, 서버에서 클라이언트가 원하는 동작을 수행한 후,

클라이언트에 (가공된)응답 데이터or (가공된)응답 메시지를 전달하면 정보를 드러내지 않으면서

 

(1)클라이언트는 원하는 동작을 요청하고, (2)서버는 요청에 대해 응답만 했기 때문에 서버 밖에서는 서버가 어떤 로직을 거쳐서 응답메시지 또는 응답데이터를 만들어내는지 알 수 없습니다. 그러므로 어플리케이션에서 API키 등 보안과 관련된 절차와 로직을 숨길 수 있습니다. 이 프로젝트를 진행하면서 중요한 부분이라고 생각했습니다.