Skip to content

Commit

Permalink
Merge pull request #42 from KUIT-Space/Feat/#41/소셜-로그인-카카오-구현
Browse files Browse the repository at this point in the history
Feat/#41/소셜 로그인 카카오 구현
  • Loading branch information
seongjunnoh authored Jul 26, 2024
2 parents e576e92 + 704f2f2 commit 368d9fb
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 36 deletions.
15 changes: 15 additions & 0 deletions src/main/java/space/space_spring/config/RestTemplateConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package space.space_spring.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
}
75 changes: 75 additions & 0 deletions src/main/java/space/space_spring/controller/OAuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package space.space_spring.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import space.space_spring.dto.oAuthInfo.KakaoInfo;
import space.space_spring.entity.User;
import space.space_spring.response.BaseResponse;
import space.space_spring.service.OAuthService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth")
@Slf4j
public class OAuthController {

private final OAuthService oAuthService;

@Value("${oauth.kakao.client.id}")
private String clientId;

@Value("${oauth.kakao.redirect.uri}")
private String redirectUri;

@Value("${oauth.kakao.client.secret}")
private String clientSecret;

/**
* 유저가 카카오 로그인 동의 시 호출될 콜백 함수
*/
@GetMapping("/callback/kakao")
public BaseResponse<String> kakaoCallback(@RequestParam(name = "code") String code, HttpServletResponse response) {

// TODO 1. 인가코드 받기
// 카카오 인증 서버는 서비스 서버의 Redirect URI로 인가 코드를 전달함
log.info("인가 코드 = {}", code);

// TODO 2. 인가코드를 기반으로 토큰(Access Token) 발급
String accessToken = null;
try {
accessToken = oAuthService.getAccessToken(code, clientId, redirectUri, clientSecret);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
log.info("accessToken = {}", accessToken);

// TODO 3. 엑세스 토큰를 통해 유저 정보 조회
KakaoInfo kakaoInfo = null;
try {
kakaoInfo = oAuthService.getKakaoInfo(accessToken);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
log.info("kakaoInfo.getEmail = {}", kakaoInfo.getEmail());
log.info("kakaoInfo.getNickname = {}", kakaoInfo.getNickName());

// TODO 4. 카카오 사용자 정보 확인
// 유저 email 정보가 db에 없을 시 -> 회원가입 & 로그인
// 유저 email 정보가 db에 있을 시 -> 로그인
User userByOAuthInfo = oAuthService.findUserByOAuthInfo(kakaoInfo);

// TODO 5. 카카오 로그인 유저에게 jwt 발급
String jwtOAuthLogin = oAuthService.provideJwtToOAuthUser(userByOAuthInfo);
response.setHeader("Authorization", "Bearer " + jwtOAuthLogin);
log.info("jwtOAuthLogin = {}", jwtOAuthLogin);

return new BaseResponse<>("카카오 로그인 성공");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package space.space_spring.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/oauth")
@Slf4j
public class TestOAuthController {

@Value("${oauth.kakao.client.id}")
private String clientId;

@Value("${oauth.kakao.redirect.uri}")
private String redirectUri;

/**
* 카카오 로그인 요청 처리
* 카카오 인증 서버의 인증 및 동의 요청 페이지로 redirect
*/
@GetMapping("/kakao")
public String kakaoConnect() {
StringBuffer url = new StringBuffer();
url.append("https://kauth.kakao.com/oauth/authorize?");
url.append("client_id="+clientId);
url.append("&redirect_uri="+redirectUri);
url.append("&response_type=code");
return "redirect:" + url.toString();
}
}
19 changes: 12 additions & 7 deletions src/main/java/space/space_spring/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,35 @@
import org.springframework.stereotype.Repository;
import space.space_spring.entity.User;
import space.space_spring.dto.user.PostUserSignupRequest;
import space.space_spring.entity.enumStatus.UserSignupType;

@Repository
public class UserDao {

@PersistenceContext
private EntityManager em;

public User saveUser(PostUserSignupRequest postUserSignupRequest) {
public User saveUser(String email, String password, String userName, UserSignupType signupType) {
User user = new User();
user.saveUser(postUserSignupRequest.getEmail(), postUserSignupRequest.getPassword(), postUserSignupRequest.getUserName());
user.saveUser(email, password, userName, signupType);

em.persist(user);
return user;
}

public User findUserByEmail(String email) {
TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :email", User.class);
public User findUserByEmailAndSignupType(String email, UserSignupType signupType) {
TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :email AND u.signupType = :signupType", User.class);
query.setParameter("email", email);
query.setParameter("signupType", signupType.getSignupType());
return query.getSingleResult();
}

public boolean hasDuplicateEmail(String email) {
String jpql = "SELECT COUNT(u) FROM User u WHERE u.email = :email";
Long count = em.createQuery(jpql, Long.class).setParameter("email", email).getSingleResult();
public boolean hasDuplicateEmail(String email, UserSignupType signupType) {
String jpql = "SELECT COUNT(u) FROM User u WHERE u.email = :email AND u.signupType = :signupType";
Long count = em.createQuery(jpql, Long.class)
.setParameter("email", email)
.setParameter("signupType", signupType.getSignupType())
.getSingleResult();
return count > 0;
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/java/space/space_spring/dto/oAuthDto/KakaoLoginDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package space.space_spring.dto.oAuthDto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class KakaoLoginDto {

private String email;

private String nickname;

public void saveKakaoLoginDto(String email, String nickname) {
this.email = email;
this.nickname = nickname;
}
}
16 changes: 16 additions & 0 deletions src/main/java/space/space_spring/dto/oAuthInfo/KakaoInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package space.space_spring.dto.oAuthInfo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class KakaoInfo {

private String nickName;

private String email;

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@
public class PostUserSignupResponse {

private Long userId;
private String jwt; // 이게 있어야할까?
}
13 changes: 5 additions & 8 deletions src/main/java/space/space_spring/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.annotation.Nullable;
import jakarta.persistence.*;
import lombok.Getter;
import space.space_spring.entity.enumStatus.UserSignupType;

@Entity
@Table(name = "Users")
Expand All @@ -22,21 +23,17 @@ public class User extends BaseEntity {
@Column(name = "user_name")
private String userName;

@Column(name = "jwt")
@Nullable
private String jwt;
@Column(name = "signup_type")
private String signupType; // 유저가 회원가입을 진행한 방식 (local, kakao, naver, google 등등)

public void saveUser(String email, String password, String userName) {
public void saveUser(String email, String password, String userName, UserSignupType signupType) {
this.email = email;
this.password = password;
this.userName = userName;
this.signupType = signupType.getSignupType();
initializeBaseEntityFields();
}

public void saveJWTtoLoginUser(String jwt) {
this.jwt = jwt;
}

public boolean passwordMatch(String password) {
return this.password.equals(password);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package space.space_spring.entity.enumStatus;

import lombok.Getter;

@Getter
public enum UserSignupType {
LOCAL("local"),
KAKAO("kakao"),
NAVER("naver"),
GOOGLE("google");

private String signupType;

UserSignupType(String signupType) {
this.signupType = signupType;
}
}
112 changes: 112 additions & 0 deletions src/main/java/space/space_spring/service/OAuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package space.space_spring.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import space.space_spring.dao.UserDao;
import space.space_spring.dto.oAuthInfo.KakaoInfo;
import space.space_spring.entity.User;
import space.space_spring.jwt.JwtLoginProvider;
import space.space_spring.util.user.UserUtils;

import java.util.UUID;

import static space.space_spring.entity.enumStatus.UserSignupType.KAKAO;

@Service
@RequiredArgsConstructor
public class OAuthService {

private final UserUtils userUtils;
private final JwtLoginProvider jwtLoginProvider;

/**
* 카카오 인증 서버가 전달해준 유저의 인가코드로 토큰 발급 요청
*/
public String getAccessToken(String code, String clientId, String redirectUri, String clientSecret) throws JsonProcessingException {

// TODO 1. HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

// TODO 2. HTTP Body 생성
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", redirectUri);
body.add("code", code);
body.add("client_secret", clientSecret);

// TODO 3. 카카오 인증 서버로 HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(body, headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);

// TODO 4. 카카오 인증 서버로부터의 HTTP 응답 (JSON) -> 액세스 토큰만 파싱
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);

return jsonNode.get("access_token").asText();
}

/**
* 카카오 인증 서버로부터 받은 access token 으로 해당 유저의 사용자 정보 가져오기
*/
public KakaoInfo getKakaoInfo(String accessToken) throws JsonProcessingException {

// TODO 1. HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

// TODO 2. 카카오 api 서버로 HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> kakaoUserInfoRequest = new HttpEntity<>(headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoUserInfoRequest,
String.class
);

// TODO 3. responseBody에 있는 정보 꺼내기
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);

// 유저의 이메일, nickname 정보 get
String email = jsonNode.get("kakao_account").get("email").asText();
String nickname = jsonNode.get("properties").get("nickname").asText();

return new KakaoInfo(nickname, email);
}

@Transactional
public User findUserByOAuthInfo(KakaoInfo kakaoInfo) {
String email = kakaoInfo.getEmail();
String nickname = kakaoInfo.getNickName();

// 카카오 서버로부터 얻은 정보로 회원가입 or 로그인
return userUtils.findOrCreateUserForOAuthInfo(email, nickname, KAKAO);
}

public String provideJwtToOAuthUser(User userByOAuthInfo) {
return jwtLoginProvider.generateToken(userByOAuthInfo);
}
}
Loading

0 comments on commit 368d9fb

Please sign in to comment.