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] : JwtFilter를 도입한다 #152

Merged
merged 14 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 0 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ WORKDIR /app
# JAR 파일만 복사
COPY ./onetime-0.0.1-SNAPSHOT.jar app.jar

# HEALTHCHECK 추가
HEALTHCHECK --interval=5s --timeout=3s --start-period=30s --retries=3 \
CMD curl --fail http://localhost:8090 || exit 1

# 애플리케이션 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]

Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ dependencies {
// Zxing
implementation 'com.google.zxing:core:3.5.1'
implementation 'com.google.zxing:javase:3.5.1'
// Health-Check
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

// QueryDSL 디렉토리
Expand Down
8 changes: 4 additions & 4 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ if [ -z "$IS_GREEN" ]; then
while true; do
echo ">>> 2. green health check 중..."
sleep 3
REQUEST=$(curl -s http://127.0.0.1:8092) # green으로 request
if [ -n "$REQUEST" ]; then
REQUEST=$(curl -s http://127.0.0.1:8092/actuator/health)
if [[ "$REQUEST" == *"UP"* ]]; then
echo "⏰ health check success!!!"
break
fi
Expand Down Expand Up @@ -82,8 +82,8 @@ else
while true; do
echo ">>> 2. blue health check 중..."
sleep 3
REQUEST=$(curl -s http://127.0.0.1:8091) # blue로 request
if [ -n "$REQUEST" ]; then
REQUEST=$(curl -s http://127.0.0.1:8091/actuator/health)
if [[ "$REQUEST" == *"UP"* ]]; then
echo "⏰ health check success!!!"
break
fi
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/side/onetime/auth/dto/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package side.onetime.auth.dto;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import side.onetime.domain.User;

import java.util.Collection;

public record CustomUserDetails(User user) implements UserDetails {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return user.getName();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

public Long getId() {
return user.getId();
}

public String getEmail() {
return user.getEmail();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package side.onetime.auth.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import side.onetime.auth.dto.CustomUserDetails;
import side.onetime.domain.User;
import side.onetime.exception.CustomException;
import side.onetime.exception.status.UserErrorStatus;
import side.onetime.repository.UserRepository;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

/**
* 사용자 이름으로 사용자 정보를 로드합니다.
*
* 데이터베이스에서 주어진 사용자 이름(username)을 기반으로 사용자를 조회하고,
* CustomUserDetails 객체로 래핑하여 반환합니다.
*
* @param username 사용자 이름
* @return 사용자 상세 정보 (CustomUserDetails 객체)
* @throws CustomException 사용자 이름에 해당하는 사용자가 없을 경우 예외를 발생시킵니다.
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByName(username)
.orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER_BY_USERNAME));
return new CustomUserDetails(user);
}

/**
* 사용자 ID로 사용자 정보를 로드합니다.
*
* 데이터베이스에서 주어진 사용자 ID를 기반으로 사용자를 조회하고,
* CustomUserDetails 객체로 래핑하여 반환합니다.
*
* @param userId 사용자 ID
* @return 사용자 상세 정보 (CustomUserDetails 객체)
* @throws CustomException 사용자 ID에 해당하는 사용자가 없을 경우 예외를 발생시킵니다.
*/
public UserDetails loadUserByUserId(Long userId) throws UsernameNotFoundException {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER_BY_USERID));
return new CustomUserDetails(user);
}
}
51 changes: 27 additions & 24 deletions src/main/java/side/onetime/controller/FixedController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import side.onetime.auth.dto.CustomUserDetails;
import side.onetime.dto.fixed.request.CreateFixedEventRequest;
import side.onetime.dto.fixed.request.ModifyFixedEventRequest;
import side.onetime.dto.fixed.response.FixedEventByDayResponse;
Expand All @@ -20,6 +22,7 @@
@RequestMapping("/api/v1/fixed-schedules")
@RequiredArgsConstructor
public class FixedController {

private final FixedEventService fixedEventService;
private final FixedScheduleService fixedScheduleService;

Expand All @@ -28,16 +31,16 @@ public class FixedController {
*
* 이 API는 새로운 고정 이벤트를 생성하고 관련된 고정 스케줄을 등록합니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param createFixedEventRequest 생성할 고정 이벤트에 대한 요청 데이터 (제목, 스케줄 목록 등)
* @param customUserDetails 인증된 사용자 정보
* @return 생성 성공 여부를 나타내는 메시지
*/
@PostMapping
public ResponseEntity<ApiResponse<Object>> createFixedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@Valid @RequestBody CreateFixedEventRequest createFixedEventRequest) {
@Valid @RequestBody CreateFixedEventRequest createFixedEventRequest,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

fixedEventService.createFixedEvent(authorizationHeader, createFixedEventRequest);
fixedEventService.createFixedEvent(customUserDetails.user(), createFixedEventRequest);
return ApiResponse.onSuccess(SuccessStatus._CREATED_FIXED_SCHEDULE);
}

Expand All @@ -46,14 +49,14 @@ public ResponseEntity<ApiResponse<Object>> createFixedEvent(
*
* 이 API는 유저가 등록한 모든 고정 스케줄을 조회합니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param customUserDetails 인증된 사용자 정보
* @return 유저가 등록한 모든 고정 스케줄 목록
*/
@GetMapping
public ResponseEntity<ApiResponse<List<FixedEventResponse>>> getAllFixedSchedules(
@RequestHeader("Authorization") String authorizationHeader) {
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

List<FixedEventResponse> fixedEventResponses = fixedScheduleService.getAllFixedSchedules(authorizationHeader);
List<FixedEventResponse> fixedEventResponses = fixedScheduleService.getAllFixedSchedules(customUserDetails.user());
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_FIXED_SCHEDULES, fixedEventResponses);
}

Expand All @@ -62,16 +65,16 @@ public ResponseEntity<ApiResponse<List<FixedEventResponse>>> getAllFixedSchedule
*
* 이 API는 특정 ID에 해당하는 고정 스케줄의 상세 정보를 조회합니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param fixedEventId 조회할 고정 스케줄의 ID
* @param customUserDetails 인증된 사용자 정보
* @return 조회된 고정 스케줄의 세부 정보
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<FixedEventDetailResponse>> getFixedScheduleDetail(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("id") Long fixedEventId) {
@PathVariable("id") Long fixedEventId,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

FixedEventDetailResponse fixedEventDetailResponse = fixedScheduleService.getFixedScheduleDetail(authorizationHeader, fixedEventId);
FixedEventDetailResponse fixedEventDetailResponse = fixedScheduleService.getFixedScheduleDetail(customUserDetails.user(), fixedEventId);
return ApiResponse.onSuccess(SuccessStatus._GET_FIXED_SCHEDULE_DETAIL, fixedEventDetailResponse);
}

Expand All @@ -80,22 +83,22 @@ public ResponseEntity<ApiResponse<FixedEventDetailResponse>> getFixedScheduleDet
*
* 이 API는 특정 고정 이벤트의 제목과 스케줄을 수정할 수 있습니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param fixedEventId 수정할 고정 이벤트의 ID
* @param modifyFixedEventRequest 수정할 고정 이벤트의 제목 및 스케줄
* @param customUserDetails 인증된 사용자 정보
* @return 수정 성공 여부를 나타내는 메시지
*/
@PatchMapping("/{id}")
public ResponseEntity<ApiResponse<Object>> modifyFixedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("id") Long fixedEventId,
@RequestBody ModifyFixedEventRequest modifyFixedEventRequest) {
@RequestBody ModifyFixedEventRequest modifyFixedEventRequest,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

if (modifyFixedEventRequest.title() != null) {
fixedEventService.modifyFixedEvent(authorizationHeader, fixedEventId, modifyFixedEventRequest);
fixedEventService.modifyFixedEvent(customUserDetails.user(), fixedEventId, modifyFixedEventRequest);
}
if (modifyFixedEventRequest.schedules() != null) {
fixedScheduleService.modifyFixedSchedule(authorizationHeader, fixedEventId, modifyFixedEventRequest);
fixedScheduleService.modifyFixedSchedule(customUserDetails.user(), fixedEventId, modifyFixedEventRequest);
}

return ApiResponse.onSuccess(SuccessStatus._MODIFY_FIXED_SCHEDULE);
Expand All @@ -106,16 +109,16 @@ public ResponseEntity<ApiResponse<Object>> modifyFixedEvent(
*
* 이 API는 특정 ID에 해당하는 고정 이벤트와 관련된 스케줄을 삭제합니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param fixedEventId 삭제할 고정 이벤트의 ID
* @param customUserDetails 인증된 사용자 정보
* @return 삭제 성공 여부를 나타내는 메시지
*/
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Object>> removeFixedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("id") Long fixedEventId) {
@PathVariable("id") Long fixedEventId,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

fixedEventService.removeFixedEvent(authorizationHeader, fixedEventId);
fixedEventService.removeFixedEvent(customUserDetails.user(), fixedEventId);
return ApiResponse.onSuccess(SuccessStatus._REMOVE_FIXED_SCHEDULE);
}

Expand All @@ -124,16 +127,16 @@ public ResponseEntity<ApiResponse<Object>> removeFixedEvent(
*
* 이 API는 특정 요일에 해당하는 고정 이벤트 목록을 조회합니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param day 조회할 요일 (예: 월, 화 등)
* @param customUserDetails 인증된 사용자 정보
* @return 조회된 요일의 고정 이벤트 목록
*/
@GetMapping("by-day/{day}")
public ResponseEntity<ApiResponse<List<FixedEventByDayResponse>>> getFixedEventByDay(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("day") String day) {
@PathVariable("day") String day,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

List<FixedEventByDayResponse> response = fixedEventService.getFixedEventByDay(authorizationHeader, day);
List<FixedEventByDayResponse> response = fixedEventService.getFixedEventByDay(customUserDetails.user(), day);
return ApiResponse.onSuccess(SuccessStatus._GET_FIXED_EVENT_BY_DAY, response);
}
}
20 changes: 11 additions & 9 deletions src/main/java/side/onetime/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import side.onetime.auth.dto.CustomUserDetails;
import side.onetime.dto.user.request.OnboardUserRequest;
import side.onetime.dto.user.request.UpdateUserProfileRequest;
import side.onetime.dto.user.response.GetUserProfileResponse;
Expand Down Expand Up @@ -40,14 +42,14 @@ public ResponseEntity<ApiResponse<OnboardUserResponse>> onboardUser(
*
* 로그인한 유저의 닉네임과 이메일 정보를 조회합니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param customUserDetails 인증된 사용자 정보
* @return 유저의 닉네임과 이메일을 포함한 응답 객체
*/
@GetMapping("/profile")
public ResponseEntity<ApiResponse<GetUserProfileResponse>> getUserProfile(
@RequestHeader("Authorization") String authorizationHeader) {
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

GetUserProfileResponse getUserProfileResponse = userService.getUserProfile(authorizationHeader);
GetUserProfileResponse getUserProfileResponse = userService.getUserProfile(customUserDetails.user());
return ApiResponse.onSuccess(SuccessStatus._GET_USER_PROFILE, getUserProfileResponse);
}

Expand All @@ -56,16 +58,16 @@ public ResponseEntity<ApiResponse<GetUserProfileResponse>> getUserProfile(
*
* 유저의 닉네임을 수정하는 API입니다. 수정된 닉네임은 최대 길이 제한을 받습니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param customUserDetails 인증된 사용자 정보
* @param updateUserProfileRequest 수정할 닉네임을 포함하는 요청 객체
* @return 성공 상태 응답 객체
*/
@PatchMapping("/profile/action-update")
public ResponseEntity<ApiResponse<SuccessStatus>> updateUserProfile(
@RequestHeader("Authorization") String authorizationHeader,
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@Valid @RequestBody UpdateUserProfileRequest updateUserProfileRequest) {

userService.updateUserProfile(authorizationHeader, updateUserProfileRequest);
userService.updateUserProfile(customUserDetails.user(), updateUserProfileRequest);
return ApiResponse.onSuccess(SuccessStatus._UPDATE_USER_PROFILE);
}

Expand All @@ -74,14 +76,14 @@ public ResponseEntity<ApiResponse<SuccessStatus>> updateUserProfile(
*
* 유저의 계정을 삭제하여 서비스에서 탈퇴하는 API입니다.
*
* @param authorizationHeader 인증된 유저의 토큰
* @param customUserDetails 인증된 사용자 정보
* @return 성공 상태 응답 객체
*/
@PostMapping("/action-withdraw")
public ResponseEntity<ApiResponse<SuccessStatus>> withdrawService(
@RequestHeader("Authorization") String authorizationHeader) {
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

userService.withdrawService(authorizationHeader);
userService.withdrawService(customUserDetails.user());
return ApiResponse.onSuccess(SuccessStatus._WITHDRAW_SERVICE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
@Getter
@RequiredArgsConstructor
public enum TokenErrorStatus implements BaseErrorCode {
_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN-001", "유효하지 않은 토큰입니다."),
_INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN-002", "유효하지 않은 리프레쉬 토큰입니다."),
_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN-003", "만료된 토큰입니다."),
_NOT_FOUND_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN-004", "리프레쉬 토큰을 찾을 수 없습니다.")
_TOKEN_SIGNATURE_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN-001", "JWT 서명이 유효하지 않습니다."),
_TOKEN_UNSUPPORTED(HttpStatus.UNAUTHORIZED, "TOKEN-002", "지원되지 않는 JWT 토큰입니다."),
_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "TOKEN-003", "만료된 토큰입니다."),
_TOKEN_MALFORMED(HttpStatus.UNAUTHORIZED, "TOKEN-004", "잘못된 JWT 토큰입니다."),
_NOT_FOUND_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN-005", "리프레쉬 토큰을 찾을 수 없습니다."),
_TOKEN_CLAIM_EXTRACTION_ERROR(HttpStatus.UNAUTHORIZED, "TOKEN-006", "토큰에서 claim 값을 추출하던 도중 에러가 발생했습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
public enum UserErrorStatus implements BaseErrorCode {
_NOT_FOUND_USER(HttpStatus.NOT_FOUND, "USER-001", "유저를 찾을 수 없습니다."),
_NICKNAME_TOO_LONG(HttpStatus.BAD_REQUEST, "USER-002", "닉네임 길이 제한을 초과했습니다."),
_NOT_FOUND_USER_BY_USERNAME(HttpStatus.NOT_FOUND, "USER-003", "username으로 user를 찾을 수 없습니다."),
_NOT_FOUND_USER_BY_USERID(HttpStatus.NOT_FOUND, "USER-004", "userId로 user를 찾을 수 없습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Loading
Loading