Spring Boot/[Spring boot] 카카오 로그인 Oauth2

[Spring boot] 카카오 로그인 Oauth 2.0

재윤 2025. 1. 5. 15:40
반응형
  • 카카오 디벨로퍼 사이트 접속

Kakao Developers

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

  • 내 애플리케이션에서 앱 이름, 사업자 정보 등 기입 사업자 정보는 그냥 앱이름이랑 같이했음

  • 이렇게 4가지가 있는데 REST API 키를 복사한다 이게 서버에서 사용할 client-id값이 된다.

  • 밑에 플랫폼에 들어가서

 

  • 이렇게 작성

 

  • 이제 여기에 들어가주면 됨

  • 동의 항목 설정

  • 이메일도 필요하니

  • 이제 활성화 설정을 on을 해주고
  • 리다이렉트 url을 이렇게 작성하는데 밑 처럼 작성을 한다.
    • Spring OAuth2 Client 라이브러리를 사용하면 기본적으로 Redirect URI을 /local/oauth2/code/{registrationId}로 Redirect 해준다. registrationId도 밑에서 알아보자.
    • http://localhost:8080/login/oauth2/code/kakao이렇게 작성해도 됨
    • 현재 작성된 거는 내가 연결한 와이파이 IP임

 

  • 위 보안 페이지로 넘어가서 밑 client secret을 생성해주자

 

  • 이제 이게 client-secret값이 된다.

 

이제 설정은 여기가 끝임.

 

코드를 작성해보자

 

  • gradle 설정
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

LoginController.java

package com.sal.saltbackend.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/login")
public class LoginController {

    
    @GetMapping
    public  String login(){
        return "login";
    }
}

 

login.html

  • 카카오 로그인을 눌렀을 때 href에 있는 걸 요청하게끔 하는 거
  • Spring OAuth2 Client 라이브러리에서 기본으로 설정된 경로이며 이 경로 또한 커스텀 가능
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <a href="/oauth2/authorization/kakao">카카오로그인</a>
</body>
</html>

 

application.properties에 밑을 넣어준다

  • 밑에 클라이언트 ID, 시크릿 값은 아까 디펠로퍼 사이트에서 생성한 값을 넣어주자 즉 client-id와 client-secret 값이다
  • 클라이언트 ID → 앱키의 REST API 키

  • 클라이언트 시크릿 값

  • redirect-url은 카카오 디벨로퍼 사이트에서 설장한 값 세팅
  • client-id와 client-secret 값은 절대 외부에 노출 시키면 안된다

 

위 코드 설명

이 설정 파일은 Spring Security OAuth2 클라이언트를 사용하여 카카오 로그인 기능을 구현하는 데 필요한 설정

 

1. spring.security.oauth2.client.registration.kakao.client-id

  • 카카오 개발자 콘솔에서 발급받은 애플리케이션의 고유 클라이언트 ID
  • 역할: OAuth2 인증 과정에서 클라이언트를 식별하는 데 사용. 카카오 서버에 인증 요청을 보낼 때 이 값을 사용하여 클라이언트를 식별

2. spring.security.oauth2.client.registration.kakao.client-secret

  • 카카오 개발자 콘솔에서 발급받은 클라이언트 비밀키
  • 역할: 클라이언트의 비밀 인증 정보로, OAuth2 인증 과정에서 클라이언트의 신뢰성을 증명하는 데 사용. 클라이언트 ID와 함께 사용되어 클라이언트를 안전하게 인증

3. spring.security.oauth2.client.registration.kakao.client-authentication-method

  • 클라이언트 인증 방식
  • 역할: 클라이언트를 인증하는 방법을 지정. 여기서는 client_secret_post 방식으로 설정되어 있는데, 이는 클라이언트 ID와 클라이언트 비밀키를 POST 요청의 본문에 포함시켜 서버에 전송하는 방식
    • 좀 더 설명 OAuth2 클라이언트가 인증 서버에 요청을 보낼 때 클라이언트를 인증하는 방법
    값의 종류:
    • client_secret_basic: 이 방식은 클라이언트 ID와 클라이언트 비밀을 HTTP 기본 인증(Basic Authentication) 헤더에 포함시켜 전송. 이 방식은 클라이언트 인증 정보를 HTTP 요청의 헤더에 Base64 인코딩된 형태로 포함시긴다.
    • client_secret_post: 이 방식은 클라이언트 ID와 클라이언트 비밀을 POST 요청의 본문에 포함시켜 전송. 클라이언트 인증 정보는 일반적인 폼 파라미터(client_id, client_secret)로 전송
    • client_secret_jwt: 클라이언트가 자신을 인증하기 위해 클라이언트 비밀과 함께 JWT(JSON Web Token)를 사용. 이 방식은 클라이언트와 인증 서버 사이에 비밀 공유가 필요.
    • private_key_jwt: 클라이언트가 자신을 인증하기 위해 공개 키를 사용한 JWT를 사용 이 방식은 클라이언트의 비밀 정보가 아닌 공개 키를 기반으로 인증이 이루어진다.
    • 사용 목적 → OAuth2 프로토콜에서 클라이언트 인증은 중요한 역할을 합니다. 인증 서버는 클라이언트를 확인하고, 올바른 클라이언트에게만 액세스 토큰을 발급해야 하기 때문에 클라이언트 인증 방법을 지정

4. spring.security.oauth2.client.registration.kakao.redirect-uri

  • OAuth2 인증 과정에서 사용자가 인증을 완료한 후 리디렉션될 URI
  • 역할: 카카오 인증 서버에서 인증을 완료한 후, 이 URI로 사용자를 리디렉션. 이 URI는 카카오 개발자 콘솔에도 등록되어 있어야 하며, 일치하지 않으면 인증 오류가 발생

5. spring.security.oauth2.client.registration.kakao.authorization-grant-type

  • OAuth2 인증 과정에서 사용할 권한 부여 방식(Grant Type)
  • 역할: 여기서는 authorization_code가 사용되고 있는데, 이는 사용자에게 카카오 로그인 화면을 제공하고, 사용자가 인증을 완료하면 인증 코드를 클라이언트에게 전달하는 방식. 클라이언트는 이 인증 코드를 사용하여 액세스 토큰을 받음.
    • OAuth2 인증 과정에서 사용되는 권한 부여 방식(Grant Type)을 지정하는 설정
    주요 값의 종류:
    • authorization_code: 이 방식은 가장 많이 사용되는 OAuth2 권한 부여 방식. 사용자가 인증 서버(예: 카카오)에서 로그인을 완료한 후, 인증 서버는 클라이언트에게 인증 코드를 발급한다. 클라이언트는 이 인증 코드를 사용하여 액세스 토큰을 요청. 이 방식은 클라이언트가 사용자로부터 직접 인증 자격 증명을 수집하지 않으므로 보안이 우수
    • implicit: 이 방식은 클라이언트가 서버 자원이 아닌 클라이언트 측(예: 자바스크립트 애플리케이션)에서 액세스 토큰을 직접 받을 수 있게 하는 방식. 인증 코드 없이 직접 액세스 토큰을 받기 때문에 보안성이 낮아 현재는 잘 사용되지 않으며, 특히 민감한 정보가 포함된 API에 대해 권장되지 않는다.
    • password: 이 방식은 리소스 소유자 비밀번호 인증 방식. 사용자가 자신의 사용자 이름과 비밀번호를 클라이언트에 직접 제공하여, 클라이언트가 이를 이용해 인증 서버에서 액세스 토큰을 요청을 한다. 이 방식은 보안이 상대적으로 낮고, 사용자의 자격 증명이 클라이언트에 노출되기 때문에 일반적으로 권장되지 않는다.
    • client_credentials: 이 방식은 클라이언트 자신이 액세스 토큰을 요청하는 방식. 이 경우, 사용자가 아닌 클라이언트가 리소스 서버에 접근할 수 있도록 액세스 토큰을 요청하는데, 보통 서버 간 통신에서 사용. 이 방식에서는 사용자 개입이 없다.
    • refresh_token: 이 방식은 기존에 발급된 액세스 토큰이 만료되었을 때, 새로운 액세스 토큰을 발급받기 위해 사용. 클라이언트는 기존의 리프레시 토큰을 사용하여 새로운 액세스 토큰을 요청할 수 있다.
    • 사용 목적 → OAuth2 인증 프로세스는 다양한 시나리오에 맞게 설계되어 있으며, 각 시나리오에 따라 적합한 권한 부여 방식을 선택

6. spring.security.oauth2.client.registration.kakao.client-name

  • 이 클라이언트 등록의 이름
  • 역할: 여러 OAuth2 클라이언트를 구성할 때 각각의 클라이언트를 구분하는 데 사용되는 이름. 이 이름은 기본적으로 UI와 같은 곳에서 사용자가 볼 수 있는 이름으로도 표시될 수 있음.

7. spring.security.oauth2.client.registration.kakao.scope

  • OAuth2 인증 과정에서 요청할 권한 범위(scope)
  • 역할: 카카오 API에서 사용자로부터 동의를 받을 때 어떤 정보에 접근할 것인지를 지정 여기서는 profile_nickname, account_email, profile_image의 세 가지 정보에 대한 접근 권한을 요청

8. spring.security.oauth2.client.provider.kakao.authorization-uri

  • 설명: 카카오의 OAuth2 인증 URL
  • 역할: 사용자를 카카오 로그인 화면으로 리디렉션할 때 사용되는 URL. 이 URL은 인증 요청을 시작하는 데 사용

9. spring.security.oauth2.client.provider.kakao.token-uri

  • 설명: 카카오의 OAuth2 토큰 발급 URL
  • 역할: 클라이언트가 인증 코드를 가지고 액세스 토큰을 요청하는 데 사용하는 URL입니다. 인증 코드가 유효하면 이 URL을 통해 액세스 토큰을 받을 수 있다.

10. spring.security.oauth2.client.provider.kakao.user-info-uri

  • 설명: 카카오의 사용자 정보 요청 URL
  • 역할: 액세스 토큰을 사용하여 카카오 서버에서 사용자의 프로필 정보를 가져올 때 사용되는 URL

11. spring.security.oauth2.client.provider.kakao.user-name-attribute

  • 카카오에서 사용자 정보를 가져올 때 사용자의 식별자(ID)로 사용할 속성
  • 역할: OAuth2User 객체의 식별자로 사용할 속성을 지정 여기서는 카카오에서 제공하는 id 속성을 사용하여 사용자 식별자로 설정

 

Oauth 2.0 방식에 대해서 자세히 설명

  • 백엔드 연결 설명 Oauth2 설명
  • 큰 틀

 

1. 카카오 로그인 요청, 인가 코드 받기 요청, 인증 및 동의 요청, 로그인 및 동의

  1. 프론트엔드에서 /oauth2/autorization/kakao 여기로 요청
  • /oauth2/autorization/kakao 왜 여기로 요청해야되냐 → Spring Security에서 자체적으로 설정 해놓음

 

그러면 사용자가 버튼을 누르면 카카오 로그인 화면을 띄워주는 놈이 누구냐 라는 것인데

  • OAuth2LoginAuthenticationFilter
    • 요청을 가로채기: 사용자가 /oauth2/authorization/kakao URL에 접근하면, OAuth2AuthorizationCodeAuthenticationProvider 들어오는 요청을 가로채고 application.properties 파일에 설정된 카카오 인증 서버 → https://kauth.kakao.com/oauth/authorize 로 리디렉션
  • OAuth2AuthorizationRequestResolver
    • OAuth2AuthorizationRequest 생성: 필터는 OAuth2AuthorizationRequestResolver를 사용하여 요청을 분석하고, OAuth2 인증 요청을 생성
  • 인증 서버로 리디렉션: 생성된 OAuth2AuthorizationRequest를 바탕으로 사용자를 카카오 인증 서버로 리디렉션.
    • 밑 주소가 카카오 서버가 프론트한테 주는 거임(Security)
    • https://accounts.kakao.com/login/?continue= ~~ 뭐시기
    • 이런걸 화면에 보여짐 이게 카카오톡 로그인 페이지
    • 로그인을 하고 이제 동의 버튼을 누르기 전 밑 도메인
    • https://kauth.kakao.com/oauth/authorize?scope=profile_nickname ~~ 뭐시기
      • 위 url의 좀 더 설명
      • scope: profile_nickname 및 account_email에 대한 접근 권한을 요청
      • response_type: code로 설정하여 인가 코드를 요청
      • state: CSRF 공격을 방지하기 위한 상태 값
      • redirect_uri: 카카오가 인증 후 인가 코드를 전송할 애플리케이션의 리디렉션 URI
      • through_account: 사용자가 로그인 페이지를 거치도록 설정
      • client_id: 카카오 API 클라이언트 ID
  • 사용자는 카카오 로그인 페이지에서 인증을 완료 후 카카오는 설정된 리디렉션 URI(http://172.20.10.12:8080/login/oauth2/code/kakao로 인가 코드 전송하고OAuth2LoginAuthenticationFilter가 인가 코드를 추출

이 모든 과정은 Spring Security가 자동으로 처리 → 코드가 없는데 다 해줌

 

2. 인가 코드 발급, 인가 코드로 토큰 발급 요청

  • OAuth2AuthorizationCodeAuthenticationProvider
    • 사용자가 로그인 및 동의까지 완료되면 추출된 인가 코드를 사용하여 카카오의 토큰 엔드포인트 (https://kauth.kakao.com/oauth/token)로 액세스 토큰을 요청.
    • application.properties → spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
    • 이것도 Security가 자동으로 해줌
    • 카카오에서 준 액세스 토큰 받는 코드(onAuthenticationSuccess 함수 안에 있음 지금은 주석)
                      //이 부분이 카카오에서 받은 토큰을 가져오는 코드임.
                    // OAuth2AuthenticationToken을 가져옴
                    OAuth2AuthenticationToken oauth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;
                    // OAuth2AuthorizedClient를 사용하여 액세스 토큰을 가져옴
                    OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(
                                   oauth2AuthenticationToken.getAuthorizedClientRegistrationId(),
                                   oauth2AuthenticationToken.getName());

 

3. 토큰으로 사용자 정보 가져오기 요청, 요청 검증 및 처리, 제공받은 사용자 정보로 서비스 회원 여부 확인

  • 그러면 토큰은 OAuth2AuthenticationToken(Security)가 자동으로 저장해주고 액세스 토큰을 받음 OAuth2UserService가 사용자 정보를 요청함.
  • 이 과정에서 DefaultOAuth2UserService의 loadUser 메서드가 호출되어 OAuth2UserRequest을통해 액세스 토큰과https://kapi.kakao.com/v2/user/me 엔드포인트로 요청을 보냄.
  • application.properties → spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me

 

그러면 이제 이 코드를 통해 oAuth2User가 등록됨

코드 설명

  • loadUser 메서드: OAuth2 인증이 성공한 후 호출되는 메서드로, 사용자 정보를 가져오는 역할. 이 메서드는 기본적으로 DefaultOAuth2UserService의 loadUser 메서드를 오버라이드
  • OAuth2UserRequest: 이 객체는 OAuth2 인증 요청에 대한 모든 정보를 포함. 예를 들어, 클라이언트의 등록 정보, 액세스 토큰, 공급자 정보 등이 포함
  • DefaultOAuth2UserService delegate: DefaultOAuth2UserService 인스턴스를 생성하여, 실제로 사용자 정보를 가져오는 역할을 위임
  • oAuth2User: DefaultOAuth2UserService에서 반환된 사용자 정보를 담고 있는 객체
  • registrationId: 현재 로그인 요청을 처리하는 OAuth2 공급자의 식별. 예를 들어, 카카오와 같은 공급자의 경우 registrationId는 "kakao" 된다
  • userNameAttributeName: OAuth2 공급자가 사용자 정보를 반환할 때, 그 중 어떤 속성을 사용자 식별자로 사용할지 결정합니다. 일반적으로 이 값은 id 또는 sub와 같은 값
  • attributes: OAuth2 공급자로부터 가져온 사용자 정보가 포함된 Map 객체입니다. 이 Map에는 사용자의 이름, 이메일, 프로필 이미지 등의 정보가 포함
  • DefaultOAuth2User: 이 객체는 OAuth2 사용자 정보를 나타내는 객체로, 사용자 식별자와 권한 정보 등을 포함
  • SimpleGrantedAuthority("ROLE_USER"): 모든 사용자에게 기본적으로 ROLE_USER 권한을 부여. 이는 사용자의 권한을 설정하는 부분
  • attributes: 위에서 가져온 사용자 정보 맵이 여기에 포함
  • userNameAttributeName: 사용자 식별자로 사용할 속성을 지정 이 속성은 DefaultOAuth2User에서 사용자 ID로 사용

OAuth2UserService.java

package com.Kkrap.Service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.Map;

//스프링 시큐리티의 DefaultOAuth2UserService를 확장하여 사용자 정보를 가져오고 처리하는 역할
//OAuth2 클라이언트의 사용자 정보를 가져오기 위한 기본 서비스 클래스 DefaultOAuth2UserService
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        Map<String, Object> attributes = oAuth2User.getAttributes();

        // 로그 출력
        logger.info("카카오로부터 불러온 사용자 정보: {}", attributes);
        System.out.println("registrationId :" + registrationId);
        System.out.println("userNameAttributeName :" + userNameAttributeName);
        System.out.println("attributes :" + attributes);
        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                attributes,
                userNameAttributeName);
    }
}

 

이렇게 하면 결과가 눈에 보일 것이다.

  • 이제 코드를 작성 파일 구조는 이렇게 됨

 

이제 SecurityConfig를 해보자

 

먼저 코드 설명

HttpSecurity 설정

  • http: 이 객체는 웹 기반 보안을 설정하는 데 사용. Spring Security의 주요 설정은 HttpSecurity를 통해 이루어진다

CSRF 설정

  • csrf(csrf -> csrf.disable()): CSRF(Cross-Site Request Forgery) 보호 기능을 비활성화. CSRF는 웹 애플리케이션에서 세션 탈취 공격을 방지하는 보안 기능

요청 권한 설정 authorizeHttpRequests

authorizeHttpRequests(authorize -> authorize

: HTTP 요청에 대한 권한 설정을 정의

  • requestMatchers("/login", "/oauth2/**").permitAll(): /login 및 /oauth2/** 경로에 대한 접근을 모든 사용자에게 허용 즉, 이 경로에 대한 요청은 인증되지 않은 사용자도 접근할 수 있습니다.
  • anyRequest().authenticated(): 위에서 명시한 경로 외의 모든 요청은 인증된 사용자만 접근할 수 있도록 제한

OAuth2 로그인 설정

  • oauth2Login(oauth2 -> oauth2: OAuth2 로그인을 설정
    • loginPage("/login"): 사용자가 로그인하지 않은 상태에서 보호된 리소스에 접근할 경우, Spring Security는 사용자를 /login 페이지로 리디렉션합니다. 이 /login 페이지는 로그인 UI를 제공하는 페이지
    • userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService)): OAuth2 인증이 성공적으로 이루어진 후, 사용자 정보를 가져오는 방법을 설정. oAuth2UserService는 이 과정에서 사용자 정보를 처리

return http.build();

  • return http.build();: 이 메서드는 HttpSecurity 설정을 마무리하고, SecurityFilterChain을 반환. Spring Security는 이 필터 체인을 사용하여 요청을 처리

 

SecurityConfig

package com.Kkrap.Security;

import com.Kkrap.Service.CustomOAuth2UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
//import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private CustomOAuth2UserService oAuth2UserService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
//                .csrf(csrf -> csrf
//                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//                )
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/login", "/oauth2/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(oAuth2UserService)
                        )
                );

        return http.build();
    }
    
}

 

그러면 이제 로그인을 하고 난 후 값을 받아보자

main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p>hihi</p>
</body>
</html>

 

SecurityConfig.java

package com.Kkrap.Security;

import com.Kkrap.Service.CustomOAuth2UserService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    private final CustomOAuth2UserService oAuth2UserService;

    public SecurityConfig(CustomOAuth2UserService oAuth2UserService) {
        this.oAuth2UserService = oAuth2UserService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(oAuth2UserService)
                        )
                        .successHandler(authenticationSuccessHandler())  // 로그인 성공 시 핸들러 사용
                );

        return http.build();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                Object principal = authentication.getPrincipal();
                if (principal instanceof DefaultOAuth2User) {
                    DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) principal;

                    // 카카오 사용자 정보 추출
                    Map<String, Object> attributes = defaultOAuth2User.getAttributes();

                    String id = attributes.get("id").toString(); // 사용자 ID
                    Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
                    Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");

                    String nickname = profile.get("nickname").toString(); // 사용자 닉네임
                    String profileImage = profile.get("profile_image_url").toString(); // 프로필 이미지 URL
                    String email = kakaoAccount.get("email").toString(); // 이메일

                    // 디버그용 로그 출력
                    System.out.println("카카오 사용자 ID: " + id);
                    System.out.println("카카오 사용자 닉네임: " + nickname);
                    System.out.println("카카오 사용자 이메일: " + email);
                    System.out.println("카카오 사용자 프로필 이미지 URL: " + profileImage);
                } else {
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid user principal type");
                }
            }
        };
    }

}

 

마지막으로 Service 폴더 안에 밑 코드를 추가해주자

CustomOAuth2UserService.java

package com.Kkrap.Service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.Map;

//스프링 시큐리티의 DefaultOAuth2UserService를 확장하여 사용자 정보를 가져오고 처리하는 역할
//OAuth2 클라이언트의 사용자 정보를 가져오기 위한 기본 서비스 클래스 DefaultOAuth2UserService
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        try {
            OAuth2User oAuth2User = super.loadUser(userRequest);
            String registrationId = userRequest.getClientRegistration().getRegistrationId();
            String userNameAttributeName = userRequest.getClientRegistration()
                    .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

            Map<String, Object> attributes = oAuth2User.getAttributes();

            // 로그 출력
            logger.info("카카오로부터 불러온 사용자 정보: {}", attributes);
            System.out.println("registrationId :" + registrationId);
            System.out.println("userNameAttributeName :" + userNameAttributeName);
            System.out.println("attributes :" + attributes);

            return new DefaultOAuth2User(
                    Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                    attributes,
                    userNameAttributeName);
            // 사용자 정보 처리 로직
        } catch (OAuth2AuthenticationException ex) {
            logger.error("OAuth2 authentication error", ex);
            throw ex;
        }

    }
}

 

반응형