Skip to content

Commit

Permalink
✨ Feat: 소셜 로그인 구현(카카오, 네이버)(#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
hyoungj00n committed Feb 5, 2024
1 parent 32d6d4f commit 8c4cc1e
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 8 deletions.
3 changes: 2 additions & 1 deletion src/main/java/com/umc/TheGoods/config/MailConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

@Component
Expand All @@ -15,11 +16,11 @@ public class MailConfig {
public boolean sendMail(String ToEmail, String code) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
message.setFrom(new InternetAddress("[email protected]", "TheGoods", "UTF-8"));
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setSubject("TheGoods 인증번호 "); // 제목
helper.setTo(ToEmail); // 받는사람
helper.setFrom("[email protected]");

String verificationCode = code;
String emailBody = String.format("TheGoods 인증번호는 %s입니다.", verificationCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,25 @@ public static Auth toEmailAuth(String email, String code, Boolean expired) {
}

public static MemberResponseDTO.EmailAuthConfirmResultDTO toEmailAuthConfirmResultDTO(Boolean checkEmail) {


return MemberResponseDTO.EmailAuthConfirmResultDTO.builder()
.checkEmail(checkEmail)
.build();
}

public static MemberResponseDTO.SocialLoginResultDTO toSocialLoginResultDTO(String result) {

return MemberResponseDTO.SocialLoginResultDTO.builder()
.result(result)
.build();
}

public static MemberResponseDTO.SocialJoinResultDTO toSocialJoinResultDTO(String phone, String email) {

return MemberResponseDTO.SocialJoinResultDTO.builder()
.phone(phone)
.email(email)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

Optional<Member> findByPhone(String phone);

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ public interface MemberCommandService {

Boolean confirmEmailAuth(MemberRequestDTO.EmailAuthConfirmDTO request);

String kakaoAuth(String code);

String naverAuth(String code, String state);


}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import com.umc.TheGoods.repository.member.CategoryRepository;
import com.umc.TheGoods.repository.member.MemberRepository;
import com.umc.TheGoods.repository.member.TermRepository;
import com.umc.TheGoods.web.dto.member.KakaoProfile;
import com.umc.TheGoods.web.dto.member.MemberRequestDTO;
import com.umc.TheGoods.web.dto.member.NaverProfile;
import com.umc.TheGoods.web.dto.member.OAuthToken;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -27,6 +30,8 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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 java.time.LocalDateTime;
Expand Down Expand Up @@ -298,6 +303,186 @@ public Boolean confirmEmailAuth(MemberRequestDTO.EmailAuthConfirmDTO request) {
return checkEmail;
}

@Override
@Transactional
public String kakaoAuth(String code) {

String jwt;

//Post방식으로 key=value 데이터를 요청
RestTemplate rt = new RestTemplate();

//HttpHeader 객체 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

//HttpBody 객체 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "49ff7dc7f5309c49f75ac2a087ffe91e");
params.add("redirect_uri", "http://localhost:8080/api/members/kakao/callback");
params.add("code", code);
//params.add("client_secret","");

//HttpHeader와 HttpBody를 하나의 객체로 담기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers);

//Http 요청해서 응답 받음
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);

ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oAuthToken = null;

try {
oAuthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

log.info("카카오 토큰:" + oAuthToken.getAccess_token());


///////////////////////////
//토큰 이용해서 정보 가져오기

RestTemplate rt2 = new RestTemplate();

HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + oAuthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");


//HttpHeader와 HttpBody를 하나의 객체로 담기
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest =
new HttpEntity<>(headers2);

ResponseEntity<String> response2 = rt2.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoProfileRequest,
String.class
);


ObjectMapper objectMapper2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;

try {
kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String phone = kakaoProfile.kakao_account.phone_number.replaceAll("[^0-9]", "");
if (phone.startsWith("82")) {
phone = "0" + phone.substring(2);
}


Optional<Member> member = memberRepository.findByPhone(phone);

if (member.isPresent()) {
List<String> roles = new ArrayList<>();
roles.add("ROLE_USER");

return createJwt(member.get().getId(), member.get().getNickname(), expiredMs, key, roles);

}

return phone + kakaoProfile.getKakao_account().email;
}

@Override
@Transactional
public String naverAuth(String code, String state) {
RestTemplate rt = new RestTemplate();


HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

//HttpBody 객체 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "t6q4Bn70dY7Kli7hS58P");
params.add("redirect_uri", "http://localhost:8080/api/members/naver/callback");
params.add("client_secret", "1uPpEHHTBF");
params.add("code", code);
params.add("state", state);


//HttpHeader와 HttpBody를 하나의 객체로 담기
HttpEntity<MultiValueMap<String, String>> naverTokenRequest =
new HttpEntity<>(params, headers);

//Http 요청해서 응답 받음
ResponseEntity<String> response = rt.exchange(
"https://nid.naver.com/oauth2.0/token",
HttpMethod.POST,
naverTokenRequest,
String.class
);

ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oAuthToken = null;

try {
oAuthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

log.info("네이버 토큰:" + oAuthToken.getAccess_token());


RestTemplate rt2 = new RestTemplate();

HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + oAuthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");


//HttpHeader와 HttpBody를 하나의 객체로 담기
HttpEntity<MultiValueMap<String, String>> naverProfileRequest =
new HttpEntity<>(headers2);

ResponseEntity<String> response2 = rt2.exchange(
"https://openapi.naver.com/v1/nid/me",
HttpMethod.POST,
naverProfileRequest,
String.class
);

log.info(response2.toString());
ObjectMapper objectMapper2 = new ObjectMapper();
NaverProfile naverProfile = null;

try {
naverProfile = objectMapper2.readValue(response2.getBody(), NaverProfile.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

String phone = naverProfile.response.mobile.replaceAll("[^0-9]", "");

Optional<Member> member = memberRepository.findByPhone(phone);

if (member.isPresent()) {
List<String> roles = new ArrayList<>();
roles.add("ROLE_USER");

return createJwt(member.get().getId(), member.get().getNickname(), expiredMs, key, roles);

}

return phone + naverProfile.getResponse().email;
}

/**
* 카테고리 validator
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

Expand All @@ -33,9 +30,6 @@ public class MemberController {

private final MemberCommandService memberCommandService;

public static final String ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
public static final String AUTH_TOKEN = "your_auth_token";


@PostMapping("/join")
@Operation(summary = "회원가입 API", description = "request 파라미터 : 닉네임, 비밀번호(String), 이메일, 생일(yyyymmdd), 성별(MALE, FEMALE, NO_SELECET), 폰번호(010xxxxxxxx),이용약관(Boolean 배열), 카테고리(Long 배열)")
Expand Down Expand Up @@ -129,6 +123,39 @@ public ApiResponse<MemberResponseDTO.EmailAuthConfirmResultDTO> emailAuth(@Reque
return ApiResponse.onSuccess(MemberConverter.toEmailAuthConfirmResultDTO(checkEmail));
}


@GetMapping("/kakao/callback")
public ApiResponse<?> kakaoCallback(@RequestParam String code) {

String result = memberCommandService.kakaoAuth(code);

//반환한 카카오 프로필에서 기존 회원이면 jwt 토큰 반환 아니면 회원가입 진행
//토큰 반환은 쉽지만 회원가입 로직으로 보내야하면 false 반환해서 회원가입 진행하도록하기

if (result.startsWith("010")) {
String phone = result.substring(0, 11);
String email = result.substring(11);
return ApiResponse.onFailure("로그인 실패", "회원가입 필요", MemberConverter.toSocialJoinResultDTO(phone, email));
}

return ApiResponse.onSuccess(MemberConverter.toSocialLoginResultDTO(result));
}

@GetMapping("/naver/callback")
public ApiResponse<?> naverCallback(@RequestParam String code, String state) {

String result = memberCommandService.naverAuth(code, state);

if (result.startsWith("010")) {
String phone = result.substring(0, 11);
String email = result.substring(11);
return ApiResponse.onFailure("로그인 실패", "회원가입 필요", MemberConverter.toSocialJoinResultDTO(phone, email));
}

return ApiResponse.onSuccess(MemberConverter.toSocialLoginResultDTO(result));
}


}


25 changes: 25 additions & 0 deletions src/main/java/com/umc/TheGoods/web/dto/member/KakaoProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.umc.TheGoods.web.dto.member;

import lombok.Data;

@Data
public class KakaoProfile {

public Long id;
public String connected_at;
public KakaoAccount kakao_account;

@Data
public class KakaoAccount {

public Boolean has_email;
public Boolean email_needs_agreement;
public Boolean is_email_valid;
public Boolean is_email_verified;
public String email;
public Boolean has_phone_number;
public Boolean phone_number_needs_agreement;
public String phone_number;

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,22 @@ public static class EmailAuthConfirmResultDTO {
Boolean checkEmail;
}


@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class SocialLoginResultDTO {
String result;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class SocialJoinResultDTO {
String phone;
String email;
}

}
Loading

0 comments on commit 8c4cc1e

Please sign in to comment.