Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] #11스프링시큐리티 예외처리 및 JWT 로직 일부 구현 #11

Merged
merged 50 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9505afe
refactor: 디렉토리 경로 수정
hyxklee Sep 30, 2024
d58103a
feat: CustomAuthenticationEntryPoint 구현
hyxklee Sep 30, 2024
e47d59a
feat: 인증 과정에서 사용할 errorMessage 구현
hyxklee Sep 30, 2024
254df65
feat: jwtFilter 구현
hyxklee Sep 30, 2024
aa83e0c
feat: jwtProvider 구현
hyxklee Sep 30, 2024
a919034
refactor: 경로 수정
hyxklee Sep 30, 2024
9690d8e
feat: jwt 관련 구현
hyxklee Oct 5, 2024
f1e8bad
feat: jwt 메시지 추가
hyxklee Oct 5, 2024
698fc8e
feat: config 수정
hyxklee Oct 5, 2024
141e0b1
refactor: 로링 제거
hyxklee Oct 5, 2024
93ffc0f
feat: 구글 소셜 로그인 관련 환경 구성
hyxklee Oct 5, 2024
3607081
feat: 구글 액세스 토큰 발급 로직 구현
hyxklee Oct 5, 2024
ca9ffc1
feat: Oauth2 의존성 추가
hyxklee Oct 5, 2024
d7bc434
feat: 액세스 토큰 발급 DTO 구현
hyxklee Oct 5, 2024
6da7ada
feat: auth code request dto 구현
hyxklee Oct 5, 2024
adf3641
feat: 엑세스 토큰으로 유저 정보 조회 기능 추가
hyxklee Oct 5, 2024
db5b2bc
feat: HttpClientErrorException 핸들러 추가
hyxklee Oct 5, 2024
204e0cf
refactor: 코드 정리
hyxklee Oct 6, 2024
1446178
feat: 구글 서버에서 받는 사용자 정보 중 필요한 정보만 저장
hyxklee Oct 6, 2024
ca7905e
refactor: 중복되는 변수 제거
hyxklee Oct 6, 2024
5025f3f
feat: 빌더 패턴 적용
hyxklee Oct 6, 2024
980ccdc
feat: 소셜 로그인시 최초 로그인(회원가입)인지 재로그인인지 구분하기 위한 상태 추가
hyxklee Oct 6, 2024
31e9bea
feat: 소셜 로그인시 최초 로그인(회원가입)인지 재로그인인지 구분하기 위한 상태 추가
hyxklee Oct 6, 2024
54899c8
feat: 소셜 로그인 경로 허용
hyxklee Oct 6, 2024
3a2ee7e
feat: getter 추가
hyxklee Oct 6, 2024
fd3d65f
feat: 유저 조회 메서드 추가
hyxklee Oct 6, 2024
11f6899
refactor: 불필요한 dto 삭제
hyxklee Oct 6, 2024
5f54cd4
refactor: 직관적인 변수명으로 수정
hyxklee Oct 6, 2024
53e738a
feat: 소셜 로그인 성공 시 상태와 발급된 토큰을 응답
hyxklee Oct 6, 2024
6d0efa9
feat: 소셜 로그인 성공 시 로그인, 회원가입을 구분해 로직 실행
hyxklee Oct 6, 2024
39d715b
feat: 소셜 로그인 성공 시 로그인, 회원가입을 구분해 로직 실행
hyxklee Oct 6, 2024
68d7e5e
feat: baseTimeEntity 사용을 위한 어노테이션 추가
hyxklee Oct 6, 2024
9202c84
refactor: 경로 수정
hyxklee Oct 6, 2024
7ae7f30
Merge branch 'refs/heads/main' into feat/#5/스프링시큐리티-예외처리
hyxklee Oct 6, 2024
e1101e3
Merge branch 'refs/heads/main' into feat/#5/스프링시큐리티-예외처리
hyxklee Oct 6, 2024
4aec592
feat: CORS 설정 추가
hyxklee Oct 6, 2024
f514ea3
refactor: Oauth2 client 의존성 제거
hyxklee Oct 7, 2024
ec853a2
refactor: 디렉토리 경로 수정
hyxklee Oct 7, 2024
3d7b268
feat: cors 재설정
hyxklee Oct 7, 2024
12d8a5c
feat: cors 재설정
hyxklee Oct 7, 2024
40f573f
feat: 프로필 초기화시 응답 코드 구현
hyxklee Oct 8, 2024
3668111
feat: 요구사항에 맞게 birth를 LocalDate로 수정 후 변경 감지를 위해 엔티티 내부 update 로직 구현
hyxklee Oct 8, 2024
1e18daf
feat: 초기 프로필 설정 api 구현
hyxklee Oct 8, 2024
45c8199
feat: 초기 프로필 설정 request dto 구현
hyxklee Oct 8, 2024
da7d4ae
feat: 초기 프로필 설정 비즈니스 로직 구현 및 주석 추가
hyxklee Oct 8, 2024
e8e7a53
refactor: 변수명 변경
hyxklee Oct 8, 2024
767ba6f
Merge branch 'refs/heads/main' into feat/#5/스프링시큐리티-예외처리
hyxklee Oct 9, 2024
cbc5155
rafactor: swagger config 수정
hyxklee Oct 9, 2024
52f16ac
Merge pull request #13 from Leets-Official/feat/#12/회원가입
hyxklee Oct 9, 2024
e025f4a
refactor:확장성을 위한 코드 수정
hyxklee Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/leets/X/XApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseTimeEntity 사용시 createdAt과 updatedAt이 자동으로 저장되는 것을 적용하기 위한 어노테이션

public class XApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
public enum ResponseMessage {


SUCCESS_SAVE(201,"회원가입에 성공했습니다.");
USER_SAVE_SUCCESS(201,"회원가입에 성공했습니다."),
LOGIN_SUCCESS(200,"로그인에 성공했습니다."),
INIT_PROFILE_SUCCESS(200, "프로필 초기설정에 성공했습니다.");

private final int code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.leets.X.domain.user.controller;

import com.leets.X.domain.user.exception.UserNotFoundException;
import com.leets.X.domain.user.dto.request.UserInitializeRequest;
import com.leets.X.domain.user.dto.request.UserSocialLoginRequest;
import com.leets.X.domain.user.dto.response.UserSocialLoginResponse;
import com.leets.X.domain.user.service.LoginStatus;
import com.leets.X.domain.user.service.UserService;
import com.leets.X.global.common.response.ResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import static com.leets.X.domain.user.controller.ResponseMessage.SUCCESS_SAVE;
import static com.leets.X.domain.user.controller.ResponseMessage.*;

// 스웨거에서 controller 단위 설명 추가
@Tag(name = "USER")
Expand All @@ -18,17 +23,28 @@
@RequestMapping("/api/v1/users")
public class UserController {

@GetMapping("/")
@Operation(summary = "테스트용 API")// 스웨거에서 API 별로 설명을 넣기 위해 사용하는 어노테이션
public String index() {
throw new UserNotFoundException();
private final UserService userService;

@PostMapping("/login")
@Operation(summary = "구글 소셜 회원가입 및 로그인")
public ResponseDto<UserSocialLoginResponse> socialLogin(@RequestBody @Valid UserSocialLoginRequest request) {
UserSocialLoginResponse response = userService.authenticate(request.authCode());
if(response.status() == LoginStatus.LOGIN){
return ResponseDto.response(LOGIN_SUCCESS.getCode(), LOGIN_SUCCESS.getMessage(), response);
}
return ResponseDto.response(USER_SAVE_SUCCESS.getCode(), USER_SAVE_SUCCESS.getMessage(), response);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프론트에서 구글 서버에서 받은 authCode로 해당 API 요청을 보냅니다
그러면 우리가 해당 코드를 다시 구글 서버에 보내 사용자 정보를 조회하고, 회원가입/로그인을 구분하여 응답을 보냅니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원가입과 로그인을 하나의 api로 통일한 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

소셜 로그인의 경우는 회원가입과 로그인이 별도로 구분하지 않는 것으로 알고 있습니다!
따라서 회원가입인지 로그인인지 프론트에서 구분할 방법은 없고, 서버 DB에 유저 정보가 있는지를 확인해서 구분해야하기 때문에 하나의 API로 사용하는 것이 맞다고 생각했습니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

알려주셔서 감사합니다! 덕분에 배워가네요 👍


// API 응답 예시를 위한 테스트 API
@GetMapping("/test")
@Operation(summary = "테스트용 API2")
public ResponseDto<Void> test() {
return ResponseDto.response(SUCCESS_SAVE.getCode(), SUCCESS_SAVE.getMessage());
/*
* JWT 인증시 Authentication 객체를 만들 때 user의 email만 넣어서 만들었기 떄문에 @AuthenticationPrincipal로 인증 정보를 가져오면 email이 반환됨
* 우리의 경우 소셜 로그인만 진행하기 때문에 이메일 중복이 없어 해당 방식도 문제 없지만, 토큰의 subject로 user_id와 email을 넣는게 나을 것 같음
* 또한 인증된 사용자 정보를 객체, email로 가져오는 것보다 id로 가져오는 것이 인덱싱 방면에서 성능이 더 좋을 듯
*/
@PatchMapping("/init")
@Operation(summary = "최초 로그인시 정보 입력")
public ResponseDto<String> initUserProfile(@RequestBody @Valid UserInitializeRequest request, @AuthenticationPrincipal @Parameter(hidden = true) String email) {// 스웨거에서 해당 정보 입력을 받지 않기 위해 hidden으로 설정
userService.initProfile(request, email);
return ResponseDto.response(INIT_PROFILE_SUCCESS.getCode(), INIT_PROFILE_SUCCESS.getMessage());
}

}
18 changes: 11 additions & 7 deletions src/main/java/com/leets/X/domain/user/domain/User.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
package com.leets.X.domain.user.domain;

import com.leets.X.domain.user.domain.enums.Gender;
import com.leets.X.domain.user.dto.request.UserInitializeRequest;
import com.leets.X.global.common.domain.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.*;

import java.time.LocalDateTime;
import java.time.LocalDate;

@Entity
// mysql에서 user 테이블이 존재 하기 때문에 다른 이름으로 지정
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter
public class User extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;

private String password;
//private String password; // 소셜로그인 구현에는 필요 x

private String customId;

Expand All @@ -35,7 +34,7 @@ public class User extends BaseTimeEntity {

private String phoneNum;

private LocalDateTime birth;
private LocalDate birth;

private String location;

Expand All @@ -47,4 +46,9 @@ public class User extends BaseTimeEntity {

// private Image image;

public void initProfile(UserInitializeRequest dto){
this.birth = dto.birth();
this.customId = dto.customId();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.leets.X.domain.user.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDate;

public record UserInitializeRequest(
// 이 둘은 필수 입력이기 떄문에 NotBlank 제약
@NotNull LocalDate birth, // 날짜의 경우 @NotNull이 적용 x
@NotBlank String customId
) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.leets.X.domain.user.dto.request;

import jakarta.validation.constraints.NotBlank;

public record UserSocialLoginRequest(
@NotBlank String authCode
) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.leets.X.domain.user.dto.response;

import com.leets.X.domain.user.service.LoginStatus;
import com.leets.X.global.auth.jwt.dto.JwtResponse;
import lombok.Builder;

@Builder
public record UserSocialLoginResponse(
Long id,
LoginStatus status,
JwtResponse jwtToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.leets.X.domain.user.exception;

import com.leets.X.global.exception.BaseException;
import com.leets.X.global.common.exception.BaseException;

import static com.leets.X.domain.user.exception.ErrorMessage.*;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
import com.leets.X.domain.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Boolean existsByEmail(String email);

Optional<User> findByEmail(String email);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.leets.X.domain.user.service;

public enum LoginStatus {
LOGIN, REGISTER
}
95 changes: 95 additions & 0 deletions src/main/java/com/leets/X/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.leets.X.domain.user.service;

import com.leets.X.domain.user.domain.User;
import com.leets.X.domain.user.dto.request.UserInitializeRequest;
import com.leets.X.domain.user.dto.response.UserSocialLoginResponse;
import com.leets.X.domain.user.exception.UserNotFoundException;
import com.leets.X.domain.user.repository.UserRepository;
import com.leets.X.global.auth.google.AuthService;
import com.leets.X.global.auth.google.dto.GoogleTokenResponse;
import com.leets.X.global.auth.google.dto.GoogleUserInfoResponse;
import com.leets.X.global.auth.jwt.JwtProvider;
import com.leets.X.global.auth.jwt.dto.JwtResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.leets.X.domain.user.service.LoginStatus.LOGIN;
import static com.leets.X.domain.user.service.LoginStatus.REGISTER;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

private final AuthService authService;
private final JwtProvider jwtProvider;
private final UserRepository userRepository;

/*
* 소셜 로그인
*/
@Transactional
public UserSocialLoginResponse authenticate(String authCode) {
GoogleTokenResponse token = authService.getGoogleAccessToken(authCode);
GoogleUserInfoResponse userInfo = authService.getGoogleUserInfo(token.access_token());

String email = userInfo.email();

// 가입된 유저라면 로그인
if (existUser(email)){
return loginUser(userInfo.email());
}
// 아니라면 회원가입
return registerUser(userInfo);
}

/*
* 회원가입 시 초기 정보 입력
*/
@Transactional
public void initProfile(UserInitializeRequest dto, String email){
User user = find(email);

user.initProfile(dto);
}

private UserSocialLoginResponse loginUser(String email) {
User user = find(email);

return new UserSocialLoginResponse(user.getId(), LOGIN, generateToken(email));
}

private UserSocialLoginResponse registerUser(GoogleUserInfoResponse userInfo) {
User user = User.builder()
.name(userInfo.name())
.email(userInfo.email())
.build();

userRepository.save(user);

return new UserSocialLoginResponse(user.getId(), REGISTER, generateToken(user.getEmail()));
}
Comment on lines +64 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드도 재사용성 관점에서 변경하면 좋을것 같아요!

  1. register 보다 create로 메서드명을 변경
  2. 리턴을 dto가 아니라 User 객체로 반환
  3. dto 로 매핑해주는 클래스, 혹은 메서드를 구현

위 방향이 User 객체가 필요할 떄 Create 메서드를 사용할 수 있어 이후 중복 코드가 줄어들 수 있을것 같습니다 !

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드는 소셜 로그인시에만 호출되는 메서드로 현재 요구사항에는 확장이 되지 않는 메서드라고 생각합니다!
자체 로그인으로 확장이 되더라도, 회원가입시 유저를 저장하는 dto 구조가 달라 소셜 회원가입과 자체 회원가입은 별도의 메서드로 관리가 되어야한다고 생각합니다!
해당 메서드의 특수성을 생각했을 때 User 객체가 필요한 상황이라면 별도의 메서드를 구현하는 것이 관리가 용이할 것 같다는 생각이 들었습니다!
제가 질문을 제대로 이해했는지 약간 의구심이 들지만 제가 했던 생각을 적어봤습니다! 감사합니다


private JwtResponse generateToken (String email){
return JwtResponse.builder()
.accessToken(jwtProvider.generateAccessToken(email))
.refreshToken(jwtProvider.generateRefreshToken())
.build();
}

/*
* userRepository에서 사용자를 검색하는 메서드
* 공통으로 사용되는 부분이 많기 때문에 별도로 분리
*/
public User find(String email){
return userRepository.findByEmail(email)
.orElseThrow(UserNotFoundException::new);
}

public boolean existUser(String email){
return userRepository.existsByEmail(email);
}

}
12 changes: 0 additions & 12 deletions src/main/java/com/leets/X/domain/user/servie/UserService.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.leets.X.global.auth.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.leets.X.global.common.response.ResponseDto;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static com.leets.X.global.auth.exception.ErrorMessage.INVALID_TOKEN;
import static com.leets.X.global.auth.exception.ErrorMessage.UNAUTHORIZED;

@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프링 시큐리티 필터에서 AuthenticationException이 발생하면 동작하는 EntryPoint를 커스텀했습니다
자세한 사항은 https://m.blog.naver.com/qjawnswkd/222303477758 참고해주세용

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT필터에서 발생하는 예외들은, 일반적인 서비스 레이어의 처리 방식과 다른것을 알게 되었습니다!


private static final String LOG_FORMAT = "Class : {}, Code : {}, Message : {}";

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Integer exceptionCode = (Integer) request.getAttribute("jwtException");

/*
* exceptionCode가 null이 아니라면 토큰 유효성 검사를 실패한 것이기 때문에 따로 처리
* exceptionCode가 null이라면 인증 정보가 없는 것이기 때문에 따로 처리
*/
if (exceptionCode != null) {
if (exceptionCode == INVALID_TOKEN.getCode()){
setResponse(response, INVALID_TOKEN.getCode(), INVALID_TOKEN.getMessage());
}
} else {
setResponse(response, UNAUTHORIZED.getCode(), UNAUTHORIZED.getMessage());
}
}

// 발생한 예외에 맞게 status를 설정하고 message를 반환
private void setResponse(HttpServletResponse response, int code, String message) throws IOException {
response.setStatus(code);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

String json = new ObjectMapper().writeValueAsString(ResponseDto.errorResponse(code, message));
response.getWriter().write(json);
}

}

Loading
Loading