Skip to content

Commit

Permalink
Merge pull request #81 from Team-Growthook/feat/#80-social-login-kaka…
Browse files Browse the repository at this point in the history
…o-api

[FEAT] 카카오 소셜 로그인 + JWT 인증
  • Loading branch information
yeseul106 authored Jan 17, 2024
2 parents 60c8dc6 + fb8e931 commit d9187e2
Show file tree
Hide file tree
Showing 22 changed files with 574 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.example.growthookserver.common.response.ApiResponse;
import com.example.growthookserver.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,6 +19,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1")
@SecurityRequirement(name = "JWT Authentication")
@Tag(name = "AciontPlan - 액션플랜 관련 API",description = "AcitonPlan APi Documnet")
public class ActionPlanController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.example.growthookserver.common.response.ApiResponse;
import com.example.growthookserver.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,6 +19,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1")
@SecurityRequirement(name = "JWT Authentication")
@Tag(name = "Cave - 동굴 관련 API", description = "Cave API Document")
public class CaveController {
private final CaveService caveService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.growthookserver.api.member.auth.controller;

import com.example.growthookserver.api.member.auth.dto.Request.AuthRequestDto;
import com.example.growthookserver.api.member.auth.dto.Response.AuthResponseDto;
import com.example.growthookserver.api.member.auth.dto.Response.AuthTokenResponseDto;
import com.example.growthookserver.api.member.auth.service.AuthService;
import com.example.growthookserver.common.config.jwt.JwtTokenProvider;
import com.example.growthookserver.common.response.ApiResponse;
import com.example.growthookserver.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Tag(name = "Auth - 인증/인가 관련 API", description = "Auth API Document")
public class AuthController {
private final AuthService authService;
private final JwtTokenProvider jwtTokenProvider;

@PostMapping()
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "SocialLogin", description = "소셜 로그인 API입니다.")
public ApiResponse<AuthResponseDto> socialLogin(@RequestBody AuthRequestDto authRequestDto) throws NoSuchAlgorithmException, InvalidKeySpecException {

AuthResponseDto responseDto = authService.socialLogin(authRequestDto);
return ApiResponse.success(SuccessStatus.SIGNIN_SUCCESS, responseDto);

}

@GetMapping("/token")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "TokenRefresh", description = "토큰 재발급 API입니다.")
public ApiResponse<AuthTokenResponseDto> getNewToken(HttpServletRequest request) {
String accessToken = (String) request.getAttribute("newAccessToken");
String refreshToken = jwtTokenProvider.resolveRefreshToken(request);

return ApiResponse.success(SuccessStatus.GET_NEW_TOKEN_SUCCESS, authService.getNewToken(accessToken, refreshToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.growthookserver.api.member.auth.dto.Request;

import static lombok.AccessLevel.PROTECTED;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = PROTECTED)
public class AuthRequestDto {
private String socialPlatform;
private String socialToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.growthookserver.api.member.auth.dto.Response;

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

@Getter
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class AuthResponseDto {

private String nickname;

private Long memberId;

private String accessToken;

private String refreshToken;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.growthookserver.api.member.auth.dto.Response;

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

@Getter
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class AuthTokenResponseDto {
private String accessToken;

private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.growthookserver.api.member.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class SocialInfoDto {
private String id;
private String nickname;
private String email;
private String profileImage;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.growthookserver.api.member.auth.service;

import com.example.growthookserver.api.member.auth.dto.Request.AuthRequestDto;
import com.example.growthookserver.api.member.auth.dto.Response.AuthResponseDto;
import com.example.growthookserver.api.member.auth.dto.Response.AuthTokenResponseDto;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

public interface AuthService {
AuthResponseDto socialLogin(AuthRequestDto authRequestDto) throws NoSuchAlgorithmException, InvalidKeySpecException;

AuthTokenResponseDto getNewToken(String accessToken, String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.example.growthookserver.api.member.auth.service.Impl;

import com.example.growthookserver.api.member.auth.SocialPlatform;
import com.example.growthookserver.api.member.auth.dto.Request.AuthRequestDto;
import com.example.growthookserver.api.member.auth.dto.Response.AuthResponseDto;
import com.example.growthookserver.api.member.auth.dto.Response.AuthTokenResponseDto;

import com.example.growthookserver.api.member.auth.dto.SocialInfoDto;
import com.example.growthookserver.api.member.auth.service.AuthService;
import com.example.growthookserver.api.member.auth.service.KakaoAuthService;

import com.example.growthookserver.api.member.domain.Member;
import com.example.growthookserver.api.member.repository.MemberRepository;
import com.example.growthookserver.common.config.jwt.JwtTokenProvider;
import com.example.growthookserver.common.config.jwt.UserAuthentication;
import com.example.growthookserver.common.exception.BadRequestException;
import com.example.growthookserver.common.response.ErrorStatus;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final JwtTokenProvider jwtTokenProvider;
private final KakaoAuthService kakaoAuthService;
private final MemberRepository memberRepository;

@Override
@Transactional
public AuthResponseDto socialLogin(AuthRequestDto authRequestDto) throws NoSuchAlgorithmException, InvalidKeySpecException {

if (authRequestDto.getSocialPlatform() == null || authRequestDto.getSocialToken() == null) {
throw new BadRequestException(ErrorStatus.VALIDATION_REQUEST_MISSING_EXCEPTION.getMessage());
}

try {
SocialPlatform socialPlatform = SocialPlatform.valueOf(authRequestDto.getSocialPlatform());

SocialInfoDto socialData = getSocialData(socialPlatform, authRequestDto.getSocialToken());

String refreshToken = jwtTokenProvider.generateRefreshToken();

Boolean isExistUser = memberRepository.existsBySocialId(socialData.getId());

// 신규 유저 저장
if (!isExistUser.booleanValue()) {
Member member = Member.builder()
.nickname(socialData.getNickname())
.email(socialData.getEmail())
.socialPlatform(socialPlatform)
.socialId(socialData.getId())
.profileImage(socialData.getProfileImage())
.build();

memberRepository.save(member);

member.updateRefreshToken(refreshToken);
}
else memberRepository.findMemberBySocialIdOrThrow(socialData.getId()).updateRefreshToken(refreshToken);

// socialId를 통해서 등록된 유저 찾기
Member signedMember = memberRepository.findMemberBySocialIdOrThrow(socialData.getId());

Authentication authentication = new UserAuthentication(signedMember.getId(), null, null);

String accessToken = jwtTokenProvider.generateAccessToken(authentication);

return AuthResponseDto.of(signedMember.getNickname(), signedMember.getId(), accessToken, signedMember.getRefreshToken());

} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException(ErrorStatus.ANOTHER_ACCESS_TOKEN.getMessage());
}
}

@Override
@Transactional
public AuthTokenResponseDto getNewToken(String accessToken, String refreshToken) {
return AuthTokenResponseDto.of(accessToken,refreshToken);
}

private SocialInfoDto getSocialData(SocialPlatform socialPlatform, String socialAccessToken) throws NoSuchAlgorithmException, InvalidKeySpecException {

switch (socialPlatform) {
case KAKAO:
return kakaoAuthService.login(socialAccessToken);
default:
throw new IllegalArgumentException(ErrorStatus.ANOTHER_ACCESS_TOKEN.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.example.growthookserver.api.member.auth.service;

import com.example.growthookserver.api.member.auth.dto.SocialInfoDto;
import com.example.growthookserver.common.exception.BaseException;
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.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@RequiredArgsConstructor
@Component
public class KakaoAuthService {

//이 login은 카카오 서버에 AccessToken으로 접속할때의 login
public SocialInfoDto login(String socialAccessToken) {
return getKakaoSocialData(socialAccessToken);
}

private SocialInfoDto getKakaoSocialData(String socialAccessToken) {

try{
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", socialAccessToken);
HttpEntity<JsonNode> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> responseData = restTemplate.postForEntity("https://kapi.kakao.com/v2/user/me", httpEntity, String.class);
ObjectMapper objectMapper = new ObjectMapper();

JsonNode jsonNode = objectMapper.readTree(responseData.getBody());

String nickname = jsonNode.get("kakao_account").get("profile").get("nickname").asText();
String profileImage;
try {
profileImage = jsonNode.get("kakao_account").get("profile").get("profile_image_url").asText();
} catch (NullPointerException e) {
profileImage = null;
}
String email = jsonNode.get("kakao_account").get("email").asText();
String kakaoId = jsonNode.get("id").asText();

return new SocialInfoDto(kakaoId, nickname, email, profileImage);
} catch (JsonProcessingException e) {
throw new BaseException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 계정 데이터 가공 실패");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.example.growthookserver.common.response.ApiResponse;
import com.example.growthookserver.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -20,6 +21,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1")
@SecurityRequirement(name = "JWT Authentication")
@Tag(name = "Member - 유저 관련 API", description = "Member API Document")
public class MemberController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public class Member extends BaseTimeEntity {
@Column
private String email;

@Column
private String profileImage;

@Enumerated(value = EnumType.STRING)
@Column(name = "social_platform")
private SocialPlatform socialPlatform;

@Column(name = "is_new_member")
private Boolean isNewMember;

@Column(name = "used_ssuk")
private Integer usedSsuk;

Expand All @@ -43,16 +43,16 @@ public class Member extends BaseTimeEntity {
private String refreshToken;

@Builder
public Member(String nickname, String email, SocialPlatform socialPlatform, Boolean isNewMember, Integer usedSsuk, Integer gatheredSsuk) {
public Member(String nickname, String email, SocialPlatform socialPlatform, String socialId, String profileImage) {
this.nickname = nickname;
this.email = email;
this.socialPlatform = socialPlatform;
this.isNewMember = isNewMember;
this.usedSsuk = usedSsuk;
this.gatheredSsuk = gatheredSsuk;
this.socialId = socialId;
this.profileImage = profileImage;
this.usedSsuk = 0;
this.gatheredSsuk = 0;
}

@Builder
public void incrementGatheredSsuk() {
this.gatheredSsuk = (this.gatheredSsuk == null ? 0 : this.gatheredSsuk) + 1;
}
Expand All @@ -61,4 +61,8 @@ public void useSsuck() {
this.gatheredSsuk--;
this.usedSsuk++;
}

public void updateRefreshToken (String refreshToken) {
this.refreshToken = refreshToken;
}
}
Loading

0 comments on commit d9187e2

Please sign in to comment.