Skip to content

Commit

Permalink
Merge pull request #11 from Leets-Official/feat/#5/스프링시큐리티-예외처리
Browse files Browse the repository at this point in the history
[feat] #11스프링시큐리티 예외처리 및 JWT 로직 일부 구현
  • Loading branch information
hyxklee authored Oct 9, 2024
2 parents 5f3d9f9 + e025f4a commit 18e0b35
Show file tree
Hide file tree
Showing 30 changed files with 588 additions and 62 deletions.
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
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);
}

// 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()));
}

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 {

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

0 comments on commit 18e0b35

Please sign in to comment.