Skip to content

Commit

Permalink
[MERGE/#10] - 소셜 로그인 로직 구현 코드 리뷰 반영 및 추가 구현 사항
Browse files Browse the repository at this point in the history
[FEAT] #10 - 소셜 로그인 로직 구현 코드 리뷰 반영 및 추가 구현 사항
  • Loading branch information
ckkim817 authored Jul 7, 2024
2 parents 3724563 + 96d99ff commit 8734a3d
Show file tree
Hide file tree
Showing 19 changed files with 100 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.domain.member.dto.LoginSuccessResponse;
import org.sopt.seonyakServer.domain.member.service.MemberService;
import org.sopt.seonyakServer.global.common.dto.ResponseDto;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberLoginRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -20,12 +20,10 @@ public class MemberController {
private final MemberService memberService;

@PostMapping("/login")
public ResponseDto<LoginSuccessResponse> login(
public ResponseEntity<LoginSuccessResponse> login(
@RequestParam final String authorizationCode,
@RequestBody @Valid final MemberLoginRequest loginRequest
) {
LoginSuccessResponse loginSuccessResponse = memberService.create(authorizationCode, loginRequest);

return ResponseDto.success(loginSuccessResponse);
return ResponseEntity.ok(memberService.create(authorizationCode, loginRequest));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sopt.seonyakServer.global.common.external.client.SocialType;

@Entity
@Getter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.seonyakServer.domain.member.model;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum SocialType {

GOOGLE("GOOGLE"),
;

private final String socialType;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.sopt.seonyakServer.domain.member.repository;

import java.util.Optional;
import org.sopt.seonyakServer.domain.member.model.Member;
import org.sopt.seonyakServer.domain.member.model.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.sopt.seonyakServer.domain.member.service;

import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.domain.member.model.Member;
import org.sopt.seonyakServer.domain.member.repository.MemberRepository;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberInfoResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MemberManagementService {
// 트랜잭션 작업을 관리할 서비스 클래스 (자기 호출 방지 및 단일 책임 원칙 준수)

private final MemberRepository memberRepository;

@Transactional
public Long createMember(final MemberInfoResponse memberInfoResponse) {
Member member = Member.of(
memberInfoResponse.socialType(),
memberInfoResponse.socialId(),
memberInfoResponse.email()
);

return memberRepository.save(member).getId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.domain.member.dto.LoginSuccessResponse;
import org.sopt.seonyakServer.domain.member.model.Member;
import org.sopt.seonyakServer.domain.member.model.SocialType;
import org.sopt.seonyakServer.domain.member.repository.MemberRepository;
import org.sopt.seonyakServer.global.auth.MemberAuthentication;
import org.sopt.seonyakServer.global.auth.jwt.JwtTokenProvider;
import org.sopt.seonyakServer.global.common.external.client.SocialType;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberInfoResponse;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberLoginRequest;
import org.sopt.seonyakServer.global.common.external.client.google.GoogleSocialService;
Expand All @@ -22,7 +22,9 @@ public class MemberService {
private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final GoogleSocialService googleSocialService;
private final MemberManagementService memberManagementService;

// JWT Access Token 생성
public LoginSuccessResponse create(
final String authorizationCode,
final MemberLoginRequest loginRequest
Expand All @@ -32,62 +34,52 @@ public LoginSuccessResponse create(
);
}

// 소셜 플랫폼으로부터 해당 유저 정보를 받아옴
public MemberInfoResponse getMemberInfoResponse(
final String authorizationCode,
final MemberLoginRequest loginRequest
) {
if (loginRequest.socialType() == SocialType.GOOGLE) {
return googleSocialService.login(authorizationCode, loginRequest);
}
throw new CustomException(ErrorType.INVALID_SOCIAL_TYPE_ERROR);
}

// Access Token을 생성할 때, 해당 유저의 회원가입 여부를 판단
private LoginSuccessResponse getTokenDto(final MemberInfoResponse memberInfoResponse) {
try {
if (isExistingMember(memberInfoResponse.socialId(), memberInfoResponse.socialType())) {
if (isExistingMember(memberInfoResponse.socialType(), memberInfoResponse.socialId())) {
return getTokenByMemberId(
getBySocialId(memberInfoResponse.socialId(), memberInfoResponse.socialType()).getId()
getMemberIdBySocialId(memberInfoResponse.socialType(), memberInfoResponse.socialId())
);
} else {
Long id = createMember(memberInfoResponse);
Long id = memberManagementService.createMember(memberInfoResponse);

return getTokenByMemberId(id);
}
} catch (DataIntegrityViolationException e) { // DB 무결성 제약 조건 위반 예외
return getTokenByMemberId(
getBySocialId(memberInfoResponse.socialId(), memberInfoResponse.socialType()).getId()
getMemberIdBySocialId(memberInfoResponse.socialType(), memberInfoResponse.socialId())
);
}
}

public MemberInfoResponse getMemberInfoResponse(
final String authorizationCode,
final MemberLoginRequest loginRequest
) {
switch (loginRequest.socialType()) {
case GOOGLE:
return googleSocialService.login(authorizationCode, loginRequest);
default:
throw new CustomException(ErrorType.INVALID_SOCIAL_TYPE_ERROR);
}
}

public boolean isExistingMember(
final String socialId,
final SocialType socialType
final SocialType socialType,
final String socialId
) {
return memberRepository.findBySocialTypeAndSocialId(socialId, socialType).isPresent();
return memberRepository.findBySocialTypeAndSocialId(socialType, socialId).isPresent();
}

public Member getBySocialId(
final String socialId,
final SocialType socialType
public Long getMemberIdBySocialId(
final SocialType socialType,
final String socialId
) {
Member member = memberRepository.findBySocialTypeAndSocialId(socialId, socialType).orElseThrow(
Member member = memberRepository.findBySocialTypeAndSocialId(socialType, socialId).orElseThrow(
() -> new CustomException(ErrorType.NOT_FOUND_MEMBER_ERROR)
);

return member;
}

public Long createMember(final MemberInfoResponse memberInfoResponse) {
Member member = Member.of(
memberInfoResponse.socialType(),
memberInfoResponse.socialId(),
memberInfoResponse.email()
);

return memberRepository.save(member).getId();
return member.getId();
}

public LoginSuccessResponse getTokenByMemberId(final Long id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
// 사용자가 인증은 되었지만 특정 리소스에 접근할 권한이 없을 때 호출

@Override
public void handle(HttpServletRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@Component
@RequiredArgsConstructor
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
// 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근하려고 할 때 호출

@Override
public void commence(HttpServletRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.IOException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sopt.seonyakServer.global.auth.MemberAuthentication;
import org.sopt.seonyakServer.global.auth.jwt.JwtTokenProvider;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
Expand All @@ -21,10 +22,12 @@

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

// 각 HTTP 요청에 대해 토큰이 유효한지 확인하고, 유효하다면 해당 사용자를 인증 설정하는 필터링 로직
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
Expand All @@ -33,25 +36,32 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,
final String token = getJwtFromRequest(request);

if (jwtTokenProvider.validateToken(token) == VALID_JWT) {
Long memberId = jwtTokenProvider.getMemberFromJwt(token);
Long memberId = jwtTokenProvider.getMemberIdFromJwt(token);

// authentication 객체 생성 -> principal에 유저정보를 담는다.
MemberAuthentication authentication = new MemberAuthentication(memberId.toString(), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
// SecurityConfig에서 permitAll을 적용해도, Spring Security의 필터 체인을 거치므로
// 여기서 바로 Exception throw를 하게 되면 permitAll과 상관 없이 ExceptionTranslationFilter로 처리가 넘어간다.
// 따라서 예외를 직접 throw로 던져주는 것이 아닌, 발생시키기만 하고 다음 필터 호출로 이어지게끔 해야 하고, (doFilter)
// 이렇게 하면 API의 permitAll 적용 여부에 따라 ExceptionTranslationFilter를 거칠지 판단하게 된다.
log.error("JwtAuthentication Authentication Exception Occurs! - {}", exception.getMessage());
}
// 다음 필터로 요청 전달 (호출)
filterChain.doFilter(request, response);
}

// Authorization 헤더에서 JWT 토큰을 추출
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");

if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring("Bearer ".length());
} else if (StringUtils.hasText(bearerToken) && !bearerToken.startsWith("Bearer ")) {
throw new CustomException(ErrorType.INVALID_JWT_SIGNATURE);
throw new CustomException(ErrorType.BEARER_LOST_ERROR);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
import java.util.Date;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@RequiredArgsConstructor
Expand All @@ -38,16 +35,6 @@ protected void init() {
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
}

private String getTokenFromHeader(final String token) {
if (!StringUtils.hasText(token)) {
throw new CustomException(ErrorType.UN_LOGIN_ERROR);
} else if (StringUtils.hasText(token) && !token.startsWith("Bearer ")) {
throw new CustomException(ErrorType.BEARER_LOST_ERROR);
}

return token.substring("Bearer ".length());
}

public String issueAccessToken(final Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
}
Expand Down Expand Up @@ -80,7 +67,7 @@ private SecretKey getSigningKey() {

public JwtValidationType validateToken(String token) {
try {
final Claims claims = getBody(getTokenFromHeader(token));
final Claims claims = getBody(token);
return JwtValidationType.VALID_JWT;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_JWT_TOKEN;
Expand All @@ -101,8 +88,8 @@ private Claims getBody(final String token) {
.getBody();
}

public Long getMemberFromJwt(String token) {
Claims claims = getBody(getTokenFromHeader(token));
public Long getMemberIdFromJwt(String token) {
Claims claims = getBody(token);

return Long.valueOf(claims.get(MEMBER_ID).toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // web Security를 사용할 수 있게
@EnableWebSecurity // WebSecurity를 사용할 수 있게
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.sopt.seonyakServer.global.common.external.client.dto;


import org.sopt.seonyakServer.global.common.external.client.SocialType;
import org.sopt.seonyakServer.domain.member.model.SocialType;

public record MemberInfoResponse(
SocialType socialType,
Expand Down
Loading

0 comments on commit 8734a3d

Please sign in to comment.