스프링 부트를 이용한 소셜 로그인 엄청 간단하게 구현 설명은 주석으로 대체!
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 |
---|