-
Notifications
You must be signed in to change notification settings - Fork 0
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
The head ref may contain hidden characters: "feat/#5/\uC2A4\uD504\uB9C1\uC2DC\uD050\uB9AC\uD2F0-\uC608\uC678\uCC98\uB9AC"
Changes from all commits
9505afe
d58103a
e47d59a
254df65
aa83e0c
a919034
9690d8e
f1e8bad
698fc8e
141e0b1
93ffc0f
3607081
ca9ffc1
d7bc434
6da7ada
adf3641
db5b2bc
204e0cf
1446178
ca7905e
5025f3f
980ccdc
31e9bea
54899c8
3a2ee7e
fd3d65f
11f6899
5f54cd4
53e738a
6d0efa9
39d715b
68d7e5e
9202c84
7ae7f30
e1101e3
4aec592
f514ea3
ec853a2
3d7b268
12d8a5c
40f573f
3668111
1e18daf
45c8199
da7d4ae
e8e7a53
767ba6f
cbc5155
52f16ac
e025f4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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") | ||
|
@@ -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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프론트에서 구글 서버에서 받은 authCode로 해당 API 요청을 보냅니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 회원가입과 로그인을 하나의 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 소셜 로그인의 경우는 회원가입과 로그인이 별도로 구분하지 않는 것으로 알고 있습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
} | ||
|
||
} |
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.leets.X.domain.user.service; | ||
|
||
public enum LoginStatus { | ||
LOGIN, REGISTER | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드도 재사용성 관점에서 변경하면 좋을것 같아요!
위 방향이 User 객체가 필요할 떄 Create 메서드를 사용할 수 있어 이후 중복 코드가 줄어들 수 있을것 같습니다 ! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 메서드는 소셜 로그인시에만 호출되는 메서드로 현재 요구사항에는 확장이 되지 않는 메서드라고 생각합니다! |
||
|
||
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); | ||
} | ||
|
||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스프링 시큐리티 필터에서 AuthenticationException이 발생하면 동작하는 EntryPoint를 커스텀했습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
|
||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
baseTimeEntity 사용시 createdAt과 updatedAt이 자동으로 저장되는 것을 적용하기 위한 어노테이션