Skip to content

Commit

Permalink
✨ [Feature] Security 설정과 Authentication 검증 어노테이션 추가를 구현했어요. (#56)
Browse files Browse the repository at this point in the history
* refactor: 패키지 변경 (#55)

* feat: Security 의존성 및 관련 설정 추가 (#55)

* feat: jwt, security 관련 에러 추가 (#55)

* refactor: Security 설정에서 permitAll 설정의 편의를 위해 엔드포인트 변경 (#55)

* feat: Security 설정 추가 이후 @AuthenticationPrincipal 을 통해 유저 검증 추가 (#55)

* feat: Swagger 에도 인증이 필요한 요청의 경우 헤더의 토큰을 통해 Security 적용되도록 수정 (#55)

* refactor: ErrorResponse 생성자 접근제어자는 private 으로 다시 원복 및 수정 - PR 리뷰 (#55)

* feat: CORS preflight 요청 처리를 위해 OPTIONS 요청 허용, 토큰 없을 경우의 에러 처리를 위한 handler 추가 - PR 리뷰 (#55)
  • Loading branch information
parkje0927 authored Jul 10, 2023
1 parent 253d715 commit 3baef52
Show file tree
Hide file tree
Showing 19 changed files with 463 additions and 107 deletions.
3 changes: 3 additions & 0 deletions oversweet-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ dependencies {
// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

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

// web
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

@Tag(name = "Test", description = "테스트 API")
@RestController
@RequestMapping("/api/v1")
@RequestMapping("/api/v1/test")
@RequiredArgsConstructor
public class TestController {

Expand All @@ -32,12 +32,12 @@ public String health() {
return "health";
}

@GetMapping("test-db-connection")
@GetMapping("/db-connection")
public String testDbConnection() {
return franchisePureService.getFranchiseName(1L);
}

@GetMapping("test-querydsl")
@GetMapping("/querydsl")
public List<String> testQuerydsl() {
return franchisePureService.getFranchiseNames(List.of(1L, 2L));
}
Expand All @@ -46,7 +46,7 @@ public List<String> testQuerydsl() {
* 1. OverSweet 을 상속 받은 TestException 검증
* 2. 정상 데이터 검증
*/
@GetMapping("/test-exception-handler/{id}")
@GetMapping("/exception-handler/{id}")
public ResponseEntity<DataResponse<TestDataResponseDto>> testExceptionHandler(@PathVariable("id") Long id) {
if (id == 1) {
throw new TestException(TEST_EXCEPTION);
Expand All @@ -58,7 +58,7 @@ public ResponseEntity<DataResponse<TestDataResponseDto>> testExceptionHandler(@P
/**
* Dto로 바인딩 되는 Validation 검증 (NotNull, NotBlank)
*/
@GetMapping("/test-exception-handler2")
@GetMapping("/exception-handler2")
public ResponseEntity<DataResponse<TestDataResponseDto>> testExceptionHandler2(@RequestBody @Validated TestDataRequestDto testDataRequestDto) {
TestDataResponseDto testDataResponseDto = new TestDataResponseDto(testDataRequestDto.getName());
return new ResponseEntity<>(DataResponse.of(HttpStatus.ACCEPTED, "응답 성공", testDataResponseDto), HttpStatus.ACCEPTED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,30 @@
import com.depromeet.oversweet.bookmark.service.FranchiseBookMarkSearchService;
import com.depromeet.oversweet.response.DataResponse;
import com.depromeet.oversweet.response.MessageResponse;
import com.depromeet.oversweet.security.service.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.HttpStatus.OK;

@Tag(name = "즐겨찾기", description = "즐겨찾기 관련 API")
@RestController
@RequestMapping("/api/v1/bookmarks")
@RequiredArgsConstructor
@SecurityRequirement(name = "accessToken")
public class BookmarkController {

private final FranchiseBookMarkSearchService franchiseBookMarkSearchService;
Expand All @@ -33,96 +42,100 @@ public class BookmarkController {

/**
* 유저가 즐겨 찾기한 프랜차이즈 목록 조회
* TODO : 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "즐겨 찾기한 프랜차이즈 목록 조회", description = "유저가 즐겨 찾기한 프랜차이즈 목록을 조회한다.")
@ApiResponses({
@ApiResponse(
responseCode = "200", description = "즐겨 찾기한 프랜차이즈 목록을 조회 성공")
})
@GetMapping("/franchises")
public ResponseEntity<DataResponse<FranchiseBookMarkedResponseDto>> searchFranchiseBookMarked() {
FranchiseBookMarkedResponseDto responseDto = franchiseBookMarkSearchService.searchFranchiseBookMarked(100L);
public ResponseEntity<DataResponse<FranchiseBookMarkedResponseDto>> searchFranchiseBookMarked(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
FranchiseBookMarkedResponseDto responseDto = franchiseBookMarkSearchService.searchFranchiseBookMarked(userDetails.getId());
return ResponseEntity.ok(DataResponse.of(OK, "즐겨 찾기한 프랜차이즈 목록 조회 성공", responseDto));
}


/**
* 유저가 즐겨 찾기한 음료 목록 조회
* TODO : 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "즐겨 찾기한 음료 목록 조회", description = "유저가 즐겨 찾기한 음료 목록을 조회한다.")
@ApiResponses({
@ApiResponse(
responseCode = "200", description = "즐겨 찾기한 음료 목록을 조회 성공")
})
@GetMapping("/drinks")
public ResponseEntity<DataResponse<DrinkBookMarkedResponseDto>> searchDrinkBookMarked() {
DrinkBookMarkedResponseDto responseDto = drinkBookMarkSearchService.searchDrinkBookMarked(100L);
public ResponseEntity<DataResponse<DrinkBookMarkedResponseDto>> searchDrinkBookMarked(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
DrinkBookMarkedResponseDto responseDto = drinkBookMarkSearchService.searchDrinkBookMarked(userDetails.getId());
return ResponseEntity.ok(DataResponse.of(OK, "즐겨 찾기한 음료 목록 조회 성공", responseDto));
}


/**
* 유저가 특정 프랜차이즈를 즐겨 찾기 할 수 있다.
* TODO : 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "프랜차이즈 즐겨 찾기", description = "유저가 특정 프랜차이즈를 즐겨 찾기 할 수 있다.")
@ApiResponses({
@ApiResponse(
responseCode = "200", description = "프랜차이즈 즐겨 찾기 성공")
})
@PostMapping("/franchises/{franchiseId}")
public ResponseEntity<MessageResponse> markFranchiseAsBookMark(@PathVariable @Parameter(description = "프랜차이즈 고유 Id") Long franchiseId) {
franchiseBookMarkRegisterService.register(100L, franchiseId);
public ResponseEntity<MessageResponse> markFranchiseAsBookMark(@PathVariable @Parameter(description = "프랜차이즈 고유 Id") Long franchiseId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
franchiseBookMarkRegisterService.register(userDetails.getId(), franchiseId);
return ResponseEntity.ok(MessageResponse.of(OK, "프랜차이즈 즐겨 찾기 등록 성공"));
}

/**
* 유저가 특정 음료를 즐겨 찾기 할 수 있다.
* TODO : 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "음료 즐겨 찾기", description = "유저가 특정 음료를 즐겨 찾기 할 수 있다.")
@ApiResponses({
@ApiResponse(
responseCode = "200", description = "음료 즐겨 찾기 성공")
})
@PostMapping("/drinks/{drinkId}")
public ResponseEntity<MessageResponse> markDrinkAsBookMark(@PathVariable @Parameter(description = "음료 고유 Id") Long drinkId) {
drinkBookMarkRegisterService.register(100L, drinkId);
public ResponseEntity<MessageResponse> markDrinkAsBookMark(@PathVariable @Parameter(description = "음료 고유 Id") Long drinkId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
drinkBookMarkRegisterService.register(userDetails.getId(), drinkId);
return ResponseEntity.ok(MessageResponse.of(OK, "음료 즐겨 찾기 등록 성공"));
}

/**
* 유저가 특정 프랜차이즈를 즐겨 찾기 해제 할 수 있다.
* TODO : 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "프랜차이즈 즐겨 찾기 해제", description = "유저가 특정 프랜차이즈를 즐겨 찾기 해제 할 수 있다.")
@ApiResponses({
@ApiResponse(
responseCode = "200", description = "프랜차이즈 즐겨 찾기 해제 성공")
})
@DeleteMapping("/franchises/{franchiseId}")
public ResponseEntity<DataResponse<FranchiseBookMarkedResponseDto>> unMarkFranchiseAsBookMark(@PathVariable @Parameter(description = "프랜차이즈 고유 Id") Long franchiseId) {
FranchiseBookMarkedResponseDto response = franchiseBookMarkRegisterService.unregister(100L, franchiseId);
public ResponseEntity<DataResponse<FranchiseBookMarkedResponseDto>> unMarkFranchiseAsBookMark(@PathVariable @Parameter(description = "프랜차이즈 고유 Id") Long franchiseId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
FranchiseBookMarkedResponseDto response = franchiseBookMarkRegisterService.unregister(userDetails.getId(), franchiseId);
return ResponseEntity.ok(DataResponse.of(OK, "프랜차이즈 즐겨 찾기 해제 성공", response));
}

/**
* 유저가 특정 음료를 즐겨 찾기 해제 할 수 있다.
* TODO : 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "음료 즐겨 찾기 해제", description = "유저가 특정 음료를 즐겨 찾기 해제 할 수 있다.")
@ApiResponses({
@ApiResponse(
responseCode = "200", description = "음료 즐겨 찾기 해제 성공")
})
@DeleteMapping("/drinks/{drinkId}")
public ResponseEntity<DataResponse<DrinkBookMarkedResponseDto>> unMarkDrinkAsBookMark(@PathVariable @Parameter(description = "음료 고유 Id") Long drinkId) {
DrinkBookMarkedResponseDto response = drinkBookMarkRegisterService.unregister(100L, drinkId);
public ResponseEntity<DataResponse<DrinkBookMarkedResponseDto>> unMarkDrinkAsBookMark(@PathVariable @Parameter(description = "음료 고유 Id") Long drinkId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
DrinkBookMarkedResponseDto response = drinkBookMarkRegisterService.unregister(userDetails.getId(), drinkId);
return ResponseEntity.ok(DataResponse.of(OK, "음료 즐겨 찾기 해제 성공", response));
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
import com.depromeet.oversweet.drink.service.DrinkDetailSearchService;
import com.depromeet.oversweet.drink.service.DrinkWeeklyStatisticsService;
import com.depromeet.oversweet.response.DataResponse;
import com.depromeet.oversweet.security.service.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -27,6 +30,7 @@
@RestController
@RequestMapping("/api/v1/drinks")
@RequiredArgsConstructor
@SecurityRequirement(name = "accessToken")
public class DrinkController {

private final DrinkDailyStatisticsService drinkDailyStatisticsService;
Expand All @@ -35,28 +39,29 @@ public class DrinkController {

/**
* 유저 하루(데일리) 먹은 당 통계 및 음료 목록 조회.
* 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "하루 당 섭취량 통계 조회", description = "유저가 하루 먹은 당 통계를 조회합니다.")
@ApiResponses(@ApiResponse(responseCode = "200", description = "유저가 하루 먹은 당 통계 조회."))
@GetMapping("/statistics/daily")
public ResponseEntity<DataResponse<DrinkDailySugarStatisticsResponse>> retrieveUserDailySugarStatistics() {
final DrinkDailySugarStatisticsResponse response = drinkDailyStatisticsService.retrieveUserDailySugarStatistics(100L);
public ResponseEntity<DataResponse<DrinkDailySugarStatisticsResponse>> retrieveUserDailySugarStatistics(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
final DrinkDailySugarStatisticsResponse response = drinkDailyStatisticsService.retrieveUserDailySugarStatistics(userDetails.getId());
return ResponseEntity.ok()
.body(DataResponse.of(HttpStatus.OK, "유저가 하루 먹은 당 통계 조회 성공", response));
}

/**
* 유저 주간 먹은 당 통계 정보 조회.
* 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user)
*/
@Operation(summary = "주간 당 섭취량 통계 조회", description = "유저의 주간 당 통계를 조회합니다.")
@ApiResponses(@ApiResponse(responseCode = "200", description = "유저가 먹은 주간 당 통계 조회."))
@GetMapping("/statistics/weekly")
public ResponseEntity<DataResponse<DrinkWeeklySugarStatisticsResponse>> retrieveUserWeeklySugarStatistics(
@RequestBody @Valid final DrinkWeeklySugarDateRequest request
@RequestBody @Valid final DrinkWeeklySugarDateRequest request,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
final DrinkWeeklySugarStatisticsResponse response = drinkWeeklyStatisticsService.retrieveUserWeeklySugarStatistics(100L, request);
final DrinkWeeklySugarStatisticsResponse response = drinkWeeklyStatisticsService.retrieveUserWeeklySugarStatistics(userDetails.getId(), request);
return ResponseEntity.ok()
.body(DataResponse.of(HttpStatus.OK, "유저가 먹은 주간 당 통계 조회 성공", response));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import com.depromeet.oversweet.member.service.MemberFacade;
import com.depromeet.oversweet.response.DataResponse;
import com.depromeet.oversweet.response.MessageResponse;
import com.depromeet.oversweet.util.JwtTokenProvider;
import com.depromeet.oversweet.util.TokenResponse;
import com.depromeet.oversweet.security.jwt.JwtTokenProvider;
import com.depromeet.oversweet.security.jwt.TokenResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
import com.depromeet.oversweet.record.dto.response.DrinkRecordSaveResponse;
import com.depromeet.oversweet.record.service.DrinkRecordSaveService;
import com.depromeet.oversweet.response.DataResponse;
import com.depromeet.oversweet.security.service.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -21,6 +24,7 @@
@RestController
@RequestMapping("/api/v1/record")
@RequiredArgsConstructor
@SecurityRequirement(name = "accessToken")
public class RecordController {

private final DrinkRecordSaveService drinkRecordSaveService;
Expand All @@ -33,9 +37,10 @@ public class RecordController {
@ApiResponses(@ApiResponse(responseCode = "201", description = "마신 음료 당 기록 성공"))
@PostMapping("/drink")
public ResponseEntity<DataResponse<DrinkRecordSaveResponse>> saveDrinkRecord(
@RequestBody @Valid final DrinkRecordSaveRequest drinkRecordSaveRequest
@RequestBody @Valid final DrinkRecordSaveRequest drinkRecordSaveRequest,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
DrinkRecordSaveResponse response = drinkRecordSaveService.saveDrinkRecord(100L, drinkRecordSaveRequest);
DrinkRecordSaveResponse response = drinkRecordSaveService.saveDrinkRecord(userDetails.getId(), drinkRecordSaveRequest);
return ResponseEntity.ok().body(DataResponse.of(HttpStatus.CREATED, "마신 음료 당 기록 성공", response));
}

Expand Down
Loading

0 comments on commit 3baef52

Please sign in to comment.