AVATAR

카카오 소셜 로그인 간단 구현 2장

jki09871 2024. 9. 13. 15:17

스프링 부트를 이용한 소셜 로그인 엄청 간단하게 구현 설명은 주석으로 대체!

 

dependencies 설정

dependencies {
    // Spring Data JPA를 사용하기 위한 의존성 (데이터베이스와 상호작용)
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // Spring Web 애플리케이션을 개발하기 위한 의존성 (웹 서비스 및 REST API 개발)
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // Lombok 라이브러리를 사용하여 코드에서 Getter, Setter, 생성자 등을 자동 생성 (빌드 시 포함되지 않음)
    compileOnly 'org.projectlombok:lombok'

    // MySQL 데이터베이스 드라이버 (실행 시 사용)
    runtimeOnly 'com.mysql:mysql-connector-j'

    // Lombok을 사용하기 위한 애노테이션 처리기
    annotationProcessor 'org.projectlombok:lombok'

    // Spring Boot에서 제공하는 기본 테스트 프레임워크 (단위 테스트와 통합 테스트 지원)
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // JUnit Platform을 런처로 사용하는 테스트 구성
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // BCrypt를 사용한 비밀번호 암호화 라이브러리
    implementation 'at.favre.lib:bcrypt:0.10.2'

    // Thymeleaf 템플릿 엔진 (Spring MVC와 함께 사용하여 HTML 렌더링)
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

    // JWT 토큰 처리 라이브러리 (JWT API)
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    // JWT 구현 라이브러리 (실행 시 사용)
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    // JWT와 JSON을 처리하기 위한 Jackson 모듈 (실행 시 사용)
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

    // JSON 처리 라이브러리
    implementation 'org.json:json:20230227'
}

 

HomeController(할게 없음)

@Controller
public class HomeController {
    @GetMapping("/")
    public String home() {
        return "index";
    }
}

 

index.html (API KEY & REDIRECT_RUI 설정 잘 해야 함)

<!-- src/main/resources/templates/login.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
<h1>카카오 로그인!</h1>
<button id="login-kakao-btn"
        onclick="location.href='https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}
'">
    <img src="/images/kakao_login_medium_narrow.png" alt="Kakao Login">
</button>
</body>
</html>

여기서 ${REST_API_KIY} 에는 전 페이지에 보여줬던 REST API키를 넣고 ${REDIRECT_URI}는 전페이지에 설정했던 URI를 넣으면된다 (당연히 ${} 지우고 넣어줘야 한다 또 https://developers.kakao.com/tool/resource/login <<이미지 본인이 원하는 이미지 크기 만들 수 있음)

 

kakaoLoginController(REDIRECT_URI 설정한거 Mapping 잘 하기)

@Log4j2 // 로그를 찍기 위해 사용하는 Lombok 어노테이션
@Controller // 이 클래스를 Spring MVC의 컨트롤러로 지정하여 HTTP 요청을 처리
@RequiredArgsConstructor // final 필드를 포함한 생성자를 Lombok이 자동으로 생성
@RequestMapping("/api") // 기본 경로를 "/api"로 설정
public class KakaoLoginController {

    private final KakaoLoginService kakaoLoginService; // KakaoLoginService 의존성 주입

    // 로그인 성공 페이지로 이동하는 메소드
    @GetMapping("/success")
    public String loginSuccess() {
        return "success"; // 로그인 성공 시 "success" 페이지를 반환
    }

    // 카카오 로그인 콜백 처리 메소드
    @GetMapping("/user/kakao/callback")
    public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
        // 카카오 로그인 서비스 호출하여 JWT 토큰 생성
        String createToken = kakaoLoginService.kakaoLogin(code, response);

        // 생성된 JWT 토큰을 쿠키에 저장
        Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, createToken.substring(7)); // "Bearer " 제거 후 쿠키에 저장
        cookie.setPath("/"); // 쿠키 경로를 애플리케이션 전체로 설정
        response.addCookie(cookie); // 응답에 쿠키 추가

        // 로그인 성공 후 "/api/success"로 리다이렉트
        return "redirect:/api/success";
    }

    // 사용자 정보를 반환하는 API 엔드포인트
    @GetMapping("/user-info")
    @ResponseBody // 이 메소드가 반환하는 객체가 HTTP 응답 본문으로 직렬화됨
    public UserInfoDto getUserInfo(@RequestParam String auth) {
        // 전달된 JWT 토큰에서 사용자 정보를 추출하여 반환
        UserInfoDto userInfo = kakaoLoginService.getUserInfo(auth);

        return userInfo; // 추출한 사용자 정보 반환
    }
}

 

 

success.html (JS 조금은 할 줄 알아야 이해하기 쉬움)

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- jQuery 라이브러리를 로드하여 페이지에서 AJAX와 DOM 조작을 쉽게 할 수 있음 -->
    <script src="https://code.jquery.com/jquery-3.7.0.min.js"
            integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
    <!-- js-cookie 라이브러리를 사용하여 브라우저 쿠키를 쉽게 다룰 수 있도록 추가 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js"></script>

    <meta charset="UTF-8">
    <title>Title</title> <!-- 페이지 제목 -->
</head>
<body>
<div id="header-title-login-user">
    <!-- 사용자 이름을 표시할 영역 -->
    이름: <span id="username"></span></br>
    <!-- 사용자 이메일을 표시할 영역 -->
    이메일: <span id="email"></span></br>
    <!-- 사용자 권한을 표시할 영역 -->
    권한: <span id="userRole"></span>
</div>
</body>
</html>
<script>
    // 문서가 준비되면 실행되는 코드
    $(document).ready(function () {

        // 쿠키에서 JWT 토큰을 가져오는 함수 호출
        const auth = getToken();

        // AJAX 요청을 사용하여 서버에서 사용자 정보를 가져옴
        $.ajax({
            type: 'GET', // HTTP GET 메소드 사용
            url: "/api/user-info", // 사용자 정보를 가져오는 API 엔드포인트
            contentType: 'application/json', // 요청의 콘텐츠 타입을 JSON으로 설정
            data : {auth : auth} // 쿠키에서 가져온 JWT 토큰을 파라미터로 전달
        }).done(function (res) {
            // 요청 성공 시 응답에서 사용자 이름과 역할(role)을 추출
            const username = res.username;
            const email = res.email;
            const userRole = res.role;

            // username 값이 있으면 HTML 요소에 사용자 이름과 역할을 표시
            if (username) {
                $('#username').text(username); // 사용자 이름을 HTML의 #username 요소에 삽입
                $('#email').text(email); // 사용자 이름을 HTML의 #username 요소에 삽입
                $('#userRole').text(userRole); // 사용자 역할을 HTML의 #userRole 요소에 삽입
            } else {
                // username이 없으면 로그인 페이지로 리다이렉트
                window.location.href = '/api/user/login-page';
            }
        }).fail(function (jqXHR) {
            // 요청 실패 시 처리
            if (jqXHR.status === 401 || jqXHR.status === 403) {
                // 인증 실패일 경우 로그아웃 처리 (로그아웃 함수 호출)
                logout();
            } else {
                // 그 외 오류 발생 시 알림 메시지 출력
                alert('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
            }
        });

        // 쿠키에서 JWT 토큰을 가져오는 함수
        function getToken() {
            // js-cookie 라이브러리를 사용하여 'Authorization' 쿠키 값을 가져옴
            let auth = Cookies.get('Authorization');

            // 쿠키에 'Authorization' 값이 없으면 빈 문자열 반환
            if (auth === undefined) {
                return '';
            }

            // 'Bearer' 접두사가 없고, 쿠키 값이 비어있지 않으면 'Bearer '를 붙여줌
            if (auth.indexOf('Bearer') === -1 && auth !== '') {
                auth = 'Bearer ' + auth;
            }

            return auth; // JWT 토큰을 반환
        }
    });
</script>

 

 

UserEntity(테이블 객체)

package com.kakao.social.login.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter // Lombok 어노테이션: 해당 클래스의 모든 필드에 대한 Getter 메서드를 자동 생성
@Entity // JPA 엔티티 클래스임을 선언하여 해당 클래스가 데이터베이스 테이블과 매핑됨을 나타냄
@Table(name = "users") // 데이터베이스에서 해당 엔티티가 매핑될 테이블명을 "users"로 설정
@NoArgsConstructor // Lombok 어노테이션: 파라미터가 없는 기본 생성자를 자동 생성

public class User {

    @Id@GeneratedValue(strategy = GenerationType.IDENTITY)
    // @Id: 해당 필드가 기본 키(primary key)임을 나타냄
    // @GeneratedValue: 기본 키가 자동으로 생성됨을 나타내며, 전략은 IDENTITY(데이터베이스에서 자동 증가하는 ID 사용)
    private Long id;

    @Column(length = 20)
    // username 필드를 데이터베이스 컬럼과 매핑하며, 문자열 최대 길이는 20자로 제한
    private String username;

    @Column(length = 100)
    // email 필드를 데이터베이스 컬럼과 매핑하며, 문자열 최대 길이는 100자로 제한
    private String email;

    @Column(length = 300)
    // password 필드를 데이터베이스 컬럼과 매핑하며, 비밀번호는 최대 300자로 제한 (암호화된 비밀번호 저장을 고려)
    private String password;

    @Column(unique = true)
    // kakaoId 필드를 데이터베이스 컬럼과 매핑하며, 해당 값은 유일해야 함 (카카오 ID는 중복 불가)
    private Long kakaoId;

    @Column(nullable = false)
    // role 필드를 데이터베이스 컬럼과 매핑하며, 값이 반드시 존재해야 함 (NULL 불가)
    @Enumerated(value = EnumType.STRING)
    // role 필드는 EnumType.STRING으로 매핑되어, 열거형의 값을 문자열로 저장함 (UserRoleEnum의 값을 DB에 저장)
    private UserRoleEnum role;

    // User 클래스의 생성자 (파라미터로 받은 값으로 사용자 객체 생성)
    public User(String username, String email,String password, Long kakaoId, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.kakaoId = kakaoId;
        this.role = role;
    }

    // 정적 팩토리 메소드: 객체 생성을 보다 명확하게 해주는 역할
    public static User createUser(String username, String email, String password, Long kakaoId, UserRoleEnum role) {
        return new User(username, email, password, kakaoId, role);
        // 새로운 User 객체를 생성하여 반환
    }

    // 카카오 ID를 업데이트하는 메소드
    public User kakaoIdUpdate(Long kakaoId) {
        this.kakaoId = kakaoId; // 기존 사용자 객체의 카카오 ID를 업데이트
        return this; // 업데이트된 객체를 반환
    }
}

 

KakaoUserInfoDto(카카오 로그인 시 데이터 저장을 위해 만듬 request역할)

@Getter // Lombok 어노테이션: 해당 클래스의 모든 필드에 대한 Getter 메서드를 자동 생성
@NoArgsConstructor // Lombok 어노테이션: 파라미터가 없는 기본 생성자를 자동 생성
public class KakaoUserInfoDto {
    // 카카오 사용자 정보 필드
    private Long id;        // 카카오 사용자 고유 ID
    private String nickname; // 카카오 사용자 닉네임
    private String email;    // 카카오 사용자 이메일

    // 모든 필드를 초기화하는 생성자
    public KakaoUserInfoDto(Long id, String nickname, String email) {
        this.id = id;           // 전달받은 ID로 필드 초기화
        this.nickname = nickname; // 전달받은 닉네임으로 필드 초기화
        this.email = email;      // 전달받은 이메일로 필드 초기화
    }
}

 

UserInfoDto (token에서 데이터 가져오려고 만듬 response역할)

 

@Getter // Lombok 어노테이션: 모든 필드에 대한 Getter 메서드를 자동 생성
@AllArgsConstructor // Lombok 어노테이션: 모든 필드를 파라미터로 받는 생성자를 자동 생성
public class UserInfoDto {
    // 사용자 정보 필드
    private String username; // 사용자 이름
    private String role;     // 사용자 역할(권한)
	private String email;    // 사용자 이메일
    // 생성자 및 Getter 메서드는 Lombok이 자동 생성
}

 

UserRoleEnum (권한 부여)

public enum UserRoleEnum {
    USER(Authority.USER),  // 사용자 권한 설정 (USER)
    ADMIN(Authority.ADMIN);  // 관리자 권한 설정 (ADMIN)

    private final String authority; // 각 열거형에 매핑된 권한 값 저장

    // 생성자: 열거형이 생성될 때 권한 값 설정
    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    // 권한 값을 반환하는 getter 메소드
    public String getAuthority() {
        return this.authority;
    }

    // 권한 상수를 포함한 정적 클래스
    public static class Authority {
        // USER 권한의 상수 값 설정 ("ROLE_USER")
        public static final String USER = "ROLE_USER";
        // ADMIN 권한의 상수 값 설정 ("ROLE_ADMIN")
        public static final String ADMIN = "ROLE_ADMIN";
    }
}

 

userRepository(DB연결)

public interface KakaoLoginRepository extends JpaRepository<User, Long> {
    // 이메일을 기반으로 사용자 정보를 조회하는 메소드
    Optional<User> findByEmail(String email);

    // 카카오 ID를 기반으로 사용자 정보를 조회하는 메소드
    Optional<User> findByKakaoId(Long kakaoId);
}

 

RestTemplate (카카오 API에 HTTP 요청을 보내고 응답을 받음)

package com.kakao.social.login.config;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                // RestTemplate 으로 외부 API 호출 시 일정 시간이 지나도 응답이 없을 때
                // 무한 대기 상태 방지를 위해 강제 종료 설정
                .setConnectTimeout(Duration.ofSeconds(5)) // 5초
                .setReadTimeout(Duration.ofSeconds(5)) // 5초
                .build();
    }
}

 

PasswordEncoder(비밀번호 암호화)

@Component // 이 클래스를 스프링 컨텍스트에 빈으로 등록하여 다른 곳에서 주입받을 수 있도록 함
public class PasswordEncoder {

    // 입력된 비밀번호를 BCrypt를 사용해 암호화하는 메소드
    public String encode(String rawPassword) {
        // BCrypt 알고리즘을 사용하여 입력된 비밀번호를 해시한 후 문자열로 반환
        // MIN_COST는 해시 비용(복잡도)을 최소로 설정하는 값
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
    }

    // 입력된 비밀번호와 해시된 비밀번호가 일치하는지 확인하는 메소드
    public boolean matches(String rawPassword, String encodedPassword) {
        // BCrypt.verifyer()를 사용해 원본 비밀번호와 암호화된 비밀번호를 비교
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified; // 일치하면 true, 일치하지 않으면 false 반환
    }
}

 

JwtUtil (JWT 설정)

import com.kakao.social.login.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j(topic = "JwtUtil") // 로그를 찍기 위해 사용하는 Lombok 어노테이션
@Component // 이 클래스를 Spring Bean으로 등록하여 다른 곳에서 사용할 수 있게 함
public class JwtUtil {
    // JWT를 담을 HTTP Header의 키 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // JWT 내에 사용자 권한 정보를 담는 키 값
    public static final String AUTHORIZATION_KEY = "auth";
    // 토큰의 접두사 (JWT 앞에 붙는 문자열)
    public static final String BEARER_PREFIX = "Bearer ";
    // 토큰의 유효시간을 30분으로 설정
    private final long TOKEN_TIME = 30 * 60 * 1000L; // 30분

    @Value("${jwt.secret.key}") // application.properties에 설정된 secret key 값을 가져옴
    private String secretKey;
    private Key key; // JWT 서명을 위한 Key 객체
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 사용할 암호화 알고리즘 설정

    @PostConstruct // 객체가 생성되고 의존성 주입이 완료된 후 호출되는 메소드
    public void init() {
        // Base64로 인코딩된 secretKey를 디코딩하여 byte 배열로 변환 후, Key 객체로 변환
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes); // HMAC SHA256 알고리즘을 사용한 Key 생성
    }

    // JWT 생성 메소드
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date(); // 현재 시간

        // JWT 생성 및 반환
        return BEARER_PREFIX + // 토큰의 접두사 "Bearer " 추가
                Jwts.builder()
                        .setSubject(username) // JWT의 제목에 사용자의 username 설정
                        .claim(AUTHORIZATION_KEY, role) // 사용자 권한 정보를 클레임에 추가
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간 설정
                        .setIssuedAt(date) // 발급 시간 설정
                        .signWith(key, signatureAlgorithm) // 서명을 위한 키와 알고리즘 설정
                        .compact(); // JWT를 생성하여 문자열로 반환
    }

    // 토큰 검증 메소드
    public boolean validateToken(String token) {
        try {
            // 토큰을 파싱하여 유효성을 검증
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true; // 토큰이 유효할 경우 true 반환
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); // 잘못된 서명
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다."); // 토큰이 만료됨
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); // 지원하지 않는 토큰
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); // 클레임이 비어있음
        }
        return false; // 토큰이 유효하지 않을 경우 false 반환
    }

    // 토큰에서 사용자 정보(클레임) 추출 메소드
    public Claims getUserInfoFromToken(String token) {
        // 토큰을 파싱하여 클레임(body) 정보를 가져옴
        return Jwts.parserBuilder()
                .setSigningKey(key) // 서명 검증을 위한 키 설정
                .build().parseClaimsJws(token).getBody(); // JWT의 클레임을 반환
    }
}

 

KakaoLoginService (핵심 기능! 외우기 금지! 이해해 보기만!!)

package com.kakao.social.login.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kakao.social.login.config.PasswordEncoder;
import com.kakao.social.login.dto.KakaoUserInfoDto;
import com.kakao.social.login.dto.UserInfoDto;
import com.kakao.social.login.entity.User;
import com.kakao.social.login.entity.UserRoleEnum;
import com.kakao.social.login.jwt.JwtUtil;
import com.kakao.social.login.repository.KakaoLoginRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.UUID;

@Log4j2 // Log4j2 라이브러리를 사용하여 로깅 기능을 제공함을 나타냄
@Service // 해당 클래스를 Spring의 서비스 레이어 컴포넌트로 지정
@RequiredArgsConstructor // final 필드를 포함한 생성자를 자동으로 생성하는 Lombok 어노테이션

public class KakaoLoginService {

    private final KakaoLoginRepository userRepository; // KakaoLoginRepository를 통해 DB 접근
    private final RestTemplate restTemplate; // 외부 API 호출을 위한 RestTemplate 객체
    private final PasswordEncoder passwordEncoder; // 비밀번호 인코딩을 위한 PasswordEncoder 객체
    private final JwtUtil jwtUtil; // JWT 토큰 관련 작업을 위한 유틸리티 클래스

    public String kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {
        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getToken(code); // 카카오 API로부터 액세스 토큰을 받아옴

        // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
        KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken); // 액세스 토큰으로 사용자 정보 요청

        // 3. 필요시 회원가입
        User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo); // 사용자가 없을 경우 회원가입 처리

        // 4. JWT 토큰 반환
        String createToken = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole(), kakaoUser.getEmail()); // 사용자 정보로 JWT 생성

        return createToken; // 생성된 JWT 토큰 반환
    }

    private String getToken(String code) throws JsonProcessingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder.fromUriString("https://kauth.kakao.com").path("/oauth/token").encode().build().toUri();
        // 카카오 API 토큰 요청을 위한 URI 생성

        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        // 요청의 Content-Type을 지정

        // HTTP Body 생성
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code"); // 인증 방식 설정
        body.add("client_id", "f9e131b799a0fab1a582e9b668ac24b5"); // 애플리케이션의 REST API 키
        body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback"); // 인증 후 리다이렉트될 URI
        body.add("code", code); // 카카오 인증 서버에서 받은 코드

        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity.post(uri).headers(headers).body(body);
        // HTTP 요청을 위한 RequestEntity 생성

        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
        // 카카오 API에 HTTP 요청을 보내고 응답을 받음

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
        // 응답을 JSON 형태로 파싱
        return jsonNode.get("access_token").asText();
        // 액세스 토큰을 추출하여 반환
    }

    private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
        // DB 에 중복된 Kakao Id 가 있는지 확인
        Long kakaoId = kakaoUserInfo.getId(); // 카카오 사용자 ID 추출
        User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);
        // 카카오 ID로 사용자 정보가 이미 있는지 확인

        if (kakaoUser == null) {
            // 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
            String kakaoEmail = kakaoUserInfo.getEmail(); // 카카오 사용자 이메일 추출
            User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
            // 같은 이메일을 가진 사용자가 DB에 있는지 확인

            if (sameEmailUser != null) {
                kakaoUser = sameEmailUser; // 이메일로 등록된 기존 사용자라면
                // 기존 회원정보에 카카오 Id 추가
                kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId); // 카카오 ID를 업데이트함
            } else {
                // 신규 회원가입
                // password: random UUID
                String password = UUID.randomUUID().toString(); // 비밀번호를 UUID로 생성
                String encodedPassword = passwordEncoder.encode(password); // 비밀번호를 인코딩

                // email: kakao email
                String email = kakaoUserInfo.getEmail(); // 카카오 사용자 이메일 추출
                kakaoUser = User.createUser(kakaoUserInfo.getNickname(), email, password, kakaoId, UserRoleEnum.USER);
                // 새로운 사용자 객체를 생성
            }

            userRepository.save(kakaoUser); // 신규 사용자 또는 업데이트된 사용자 정보를 DB에 저장
        }
        return kakaoUser; // 사용자 객체 반환
    }

    private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {

        // 요청 URL 만들기
        URI uri = UriComponentsBuilder.fromUriString("https://kapi.kakao.com").path("/v2/user/me").encode().build().toUri();
        // 카카오 사용자 정보 요청을 위한 URI 생성

        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken); // 액세스 토큰을 Authorization 헤더에 추가
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        // 요청의 Content-Type을 지정

        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity.post(uri).headers(headers).body(new LinkedMultiValueMap<>());
        // HTTP 요청을 위한 RequestEntity 생성

        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
        // 카카오 API에 HTTP 요청을 보내고 응답을 받음

        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
        // 응답을 JSON 형태로 파싱
        Long id = jsonNode.get("id").asLong(); // 사용자 ID 추출
        String username = jsonNode.get("properties").get("nickname").asText(); // 사용자 닉네임 추출
        String email = jsonNode.get("kakao_account").get("email").asText(); // 사용자 이메일 추출

        log.info("카카오 사용자 정보: " + id + ", " + username + ", " + email);
        // 사용자 정보를 로그로 출력
        return new KakaoUserInfoDto(id, username, email); // 사용자 정보를 담은 DTO 반환
    }

    public UserInfoDto getUserInfo(String auth) {
        // JWT에서 Bearer 접두사 제거
        String token = auth.substring(7); // Authorization 헤더에서 Bearer 부분을 제외한 JWT 토큰 추출

        jwtUtil.validateToken(token); // 토큰의 유효성을 검사

        // 토큰에서 사용자 정보 추출
        Claims claims = jwtUtil.getUserInfoFromToken(token); // JWT에서 클레임(사용자 정보) 추출

        // 클레임에서 사용자 이름과 권한을 추출
        String username = claims.getSubject(); // 클레임에서 사용자 이름을 추출
        String role = claims.get(JwtUtil.AUTHORIZATION_KEY, String.class); // 클레임에서 사용자 권한을 추출
        String email = claims.get("email", String.class); // 클레임에서 이메일을 추출

        // 사용자 정보 DTO 생성
        return new UserInfoDto(username, role, email); // 사용자 이름과 권한을 포함한 DTO 반환
    }
}

 

 

결론

정말 간단하게 구현했다. 

보안적으로 문제가 많으니 그대로 가져다 쓰는것은 옳지 않다.

참고용으로만 쓰기를 바란다...

'AVATAR' 카테고리의 다른 글

카카오 소셜 로그인 간단 구현 1장  (0) 2024.09.13