Skip to content

Commit

Permalink
[MERGE/#10] - 소셜 로그인 로직 구현
Browse files Browse the repository at this point in the history
[FEAT] #10 - 소셜 로그인 로직 구현 (구글)
  • Loading branch information
seokbeom00 authored Jul 6, 2024
2 parents 6a24f85 + a7c2b72 commit 3724563
Show file tree
Hide file tree
Showing 32 changed files with 798 additions and 14 deletions.
39 changes: 35 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ repositories {
}

dependencies {
// Spring WEB
implementation 'org.springframework.boot:spring-boot-starter-web'

// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// PostgreSql
implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3'

// Spring WEB
implementation 'org.springframework.boot:spring-boot-starter-web'

//Actuator
// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// Lombok
Expand All @@ -47,6 +50,34 @@ dependencies {
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// Open Feign (External API)
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2'

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' // 쿼리 파라미터 로그 남기기
}

ext {
set('springCloudVersion', "2023.0.2")
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.sopt.seonyakServer.domain.member.controller;

import jakarta.validation.Valid;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class MemberController {

private final MemberService memberService;

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

return ResponseDto.success(loginSuccessResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.seonyakServer.domain.member.dto;

public record LoginSuccessResponse(
String accessToken
) {
public static LoginSuccessResponse of(final String accessToken) {
return new LoginSuccessResponse(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.sopt.seonyakServer.domain.member.model;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sopt.seonyakServer.global.common.external.client.SocialType;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
private SocialType socialType;

private String socialId;

private String email;

public static Member of(
final SocialType socialType,
final String socialId,
final String email
) {
return Member.builder()
.socialType(socialType)
.socialId(socialId)
.email(email)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.sopt.seonyakServer.domain.member.repository;

import org.sopt.seonyakServer.domain.member.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.seonyakServer.domain.member.repository;

import java.util.Optional;
import org.sopt.seonyakServer.domain.member.model.Member;
import org.sopt.seonyakServer.global.common.external.client.SocialType;

public interface MemberRepositoryCustom {

Optional<Member> findBySocialTypeAndSocialId(
final String socialId,
final SocialType socialType
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.sopt.seonyakServer.domain.member.repository;

import static org.sopt.seonyakServer.domain.member.model.QMember.member;

import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.domain.member.model.Member;
import org.sopt.seonyakServer.global.common.external.client.SocialType;

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

private final JPAQueryFactory jpaQueryFactory;

@Override
public Optional<Member> findBySocialTypeAndSocialId(
final String socialId,
final SocialType socialType
) {
return Optional.ofNullable(
jpaQueryFactory.selectFrom(member)
.where(
member.socialId.eq(socialId),
member.socialType.eq(socialType)
)
.fetchOne()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.sopt.seonyakServer.domain.member.service;

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.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;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final GoogleSocialService googleSocialService;

public LoginSuccessResponse create(
final String authorizationCode,
final MemberLoginRequest loginRequest
) {
return getTokenDto(
getMemberInfoResponse(authorizationCode, loginRequest)
);
}

private LoginSuccessResponse getTokenDto(final MemberInfoResponse memberInfoResponse) {
try {
if (isExistingMember(memberInfoResponse.socialId(), memberInfoResponse.socialType())) {
return getTokenByMemberId(
getBySocialId(memberInfoResponse.socialId(), memberInfoResponse.socialType()).getId()
);
} else {
Long id = createMember(memberInfoResponse);

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

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
) {
return memberRepository.findBySocialTypeAndSocialId(socialId, socialType).isPresent();
}

public Member getBySocialId(
final String socialId,
final SocialType socialType
) {
Member member = memberRepository.findBySocialTypeAndSocialId(socialId, socialType).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();
}

public LoginSuccessResponse getTokenByMemberId(final Long id) {
MemberAuthentication memberAuthentication = new MemberAuthentication(id, null, null);

return LoginSuccessResponse.of(jwtTokenProvider.issueAccessToken(memberAuthentication));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.seonyakServer.global.auth;

import java.util.Collection;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

public class MemberAuthentication extends UsernamePasswordAuthenticationToken {

// 사용자 인증 객체 생성
public MemberAuthentication(Object principal,
Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.seonyakServer.global.auth.filter;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
setResponse(response);
}

private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.seonyakServer.global.auth.filter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) {
setResponse(response);
}

private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
Loading

0 comments on commit 3724563

Please sign in to comment.