From 4559af40f1ae173a118c89ee7e32230c14cb61fb Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Thu, 21 Dec 2023 21:34:15 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20#45=20URL=20pattern=20&&=20MethodSe?= =?UTF-8?q?curity=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fitapet/domain/member/api/AccountApi.java | 39 +++++++++++++------ .../fitapet/domain/member/api/AuthApi.java | 6 +++ .../component/MemberAccountService.java | 15 +++---- .../fitapet/domain/oauth2/api/OAuthApi.java | 15 +++++++ .../common/response/SuccessResponse.java | 2 +- .../security/filter/JwtExceptionFilter.java | 2 +- .../security/oauth2/OAuth2Provider.java | 1 + .../util/jwt/exception/AuthErrorResponse.java | 2 +- .../config/security/SecurityConfig.java | 15 +------ 9 files changed, 63 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/kcy/fitapet/domain/oauth2/api/OAuthApi.java diff --git a/src/main/java/com/kcy/fitapet/domain/member/api/AccountApi.java b/src/main/java/com/kcy/fitapet/domain/member/api/AccountApi.java index 805bc129..e36b8bc2 100644 --- a/src/main/java/com/kcy/fitapet/domain/member/api/AccountApi.java +++ b/src/main/java/com/kcy/fitapet/domain/member/api/AccountApi.java @@ -9,6 +9,8 @@ import com.kcy.fitapet.domain.member.type.MemberAttrType; import com.kcy.fitapet.domain.notification.type.NotificationType; import com.kcy.fitapet.global.common.response.SuccessResponse; +import com.kcy.fitapet.global.common.response.code.ErrorCode; +import com.kcy.fitapet.global.common.response.code.StatusCode; import com.kcy.fitapet.global.common.response.exception.GlobalErrorException; import com.kcy.fitapet.global.common.security.authentication.CustomUserDetails; import com.kcy.fitapet.global.common.redis.sms.SmsPrefix; @@ -20,6 +22,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -34,9 +38,12 @@ public class AccountApi { private final MemberAccountService memberAccountService; @Operation(summary = "프로필 조회") - @GetMapping("") - public ResponseEntity getProfile(@AuthenticationPrincipal CustomUserDetails user) { - AccountProfileRes member = memberAccountService.getProfile(user.getUserId()); + @GetMapping("/{id}") + @PreAuthorize("isAuthenticated() and #id == principal.userId") + public ResponseEntity getProfile( + @PathVariable("id") Long id, + @AuthenticationPrincipal CustomUserDetails user) { + AccountProfileRes member = memberAccountService.getProfile(id, user.getUserId()); return ResponseEntity.ok(SuccessResponse.from(member)); } @@ -45,6 +52,7 @@ public ResponseEntity getProfile(@AuthenticationPrincipal CustomUserDetails u @Parameters({ @Parameter(name = "uid", description = "확인할 ID", required = true) }) + @PreAuthorize("isAnonymous()") public ResponseEntity getExistsUid(@RequestParam("uid") @NotBlank String uid) { boolean exists = memberAccountService.existsUid(uid); return ResponseEntity.ok(SuccessResponse.from(Map.of("valid", exists))); @@ -52,17 +60,20 @@ public ResponseEntity getExistsUid(@RequestParam("uid") @NotBlank String uid) @Operation(summary = "프로필(비밀번호/이름) 수정") @Parameters({ + @Parameter(name = "id", description = "수정할 프로필 ID", required = true), @Parameter(name = "type", description = "수정할 프로필 타입", required = true), @Parameter(name = "req", description = "수정할 프로필 정보") }) - @PutMapping("") + @PutMapping("/{id}") + @PreAuthorize("isAuthenticated() and #id == principal.userId") public ResponseEntity putProfile( + @PathVariable Long id, @AuthenticationPrincipal CustomUserDetails user, @RequestParam("type") @NotBlank MemberAttrType type, @RequestBody ProfilePatchReq req ) { log.info("type: {}", type); - memberAccountService.updateProfile(user.getUserId(), req, type); + memberAccountService.updateProfile(id, user.getUserId(), req, type); return ResponseEntity.ok(SuccessResponse.noContent()); } @@ -74,6 +85,7 @@ public ResponseEntity putProfile( @Parameter(name = "code", description = "인증번호", required = true), @Parameter(name = "req", description = "uid(이름)/password(비밀번호)") }) + @PreAuthorize("isAnonymous()") public ResponseEntity postSearchIdOrPassword( @RequestParam("type") @NotBlank SmsPrefix type, @RequestParam("code") @NotBlank String code, @@ -89,12 +101,17 @@ public ResponseEntity postSearchIdOrPassword( } @Operation(summary = "알림 on/off") - @Parameter(name = "type", description = "알림 타입", example = "care or memo or schedule", required = true) - @GetMapping("/notify") - public ResponseEntity putNotify(@AuthenticationPrincipal CustomUserDetails user, @RequestParam("type") @NotBlank NotificationType type) { - memberAccountService.updateNotification(user.getUserId(), type); + @Parameters({ + @Parameter(name = "id", description = "알림 설정할 ID", required = true), + @Parameter(name = "type", description = "알림 타입", example = "care or memo or schedule", required = true) + }) + @GetMapping("/{id}/notify") + @PreAuthorize("isAuthenticated() and #id == principal.userId") + public ResponseEntity putNotify( + @PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails user, + @RequestParam("type") @NotBlank NotificationType type) { + memberAccountService.updateNotification(id, user.getUserId(), type); return ResponseEntity.ok(SuccessResponse.noContent()); } - - } diff --git a/src/main/java/com/kcy/fitapet/domain/member/api/AuthApi.java b/src/main/java/com/kcy/fitapet/domain/member/api/AuthApi.java index 4f74b588..5bfb6c62 100644 --- a/src/main/java/com/kcy/fitapet/domain/member/api/AuthApi.java +++ b/src/main/java/com/kcy/fitapet/domain/member/api/AuthApi.java @@ -36,6 +36,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; @@ -64,6 +65,7 @@ public class AuthApi { @ApiResponse(responseCode = "4xx", description = "에러", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) @PostMapping("/register") + @PreAuthorize("isAnonymous()") public ResponseEntity signUp(@RequestHeader("Authorization") @NotBlank String accessToken, @RequestBody @Valid SignUpReq dto) { Map tokens = memberAuthService.register(accessToken, dto); return getResponseEntity(tokens); @@ -80,6 +82,7 @@ public ResponseEntity signUp(@RequestHeader("Authorization") @NotBlank String @ApiResponse(responseCode = "4xx", description = "에러", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) @PostMapping("/register-sms") + @PreAuthorize("isAnonymous()") public ResponseEntity registerSmsAuthorization( @RequestParam(value = "code", required = false) String code, @RequestBody @Valid SmsReq dto) { @@ -108,6 +111,7 @@ public ResponseEntity registerSmsAuthorization( @ApiResponse(responseCode = "4xx", description = "에러", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) @PostMapping("/search-sms") + @PreAuthorize("isAnonymous()") public ResponseEntity searchSmsAuthorization( @RequestParam(value = "type") SmsPrefix type, @RequestParam(value = "code", required = false) String code, @@ -131,6 +135,7 @@ public ResponseEntity searchSmsAuthorization( @ApiResponse(responseCode = "4xx", description = "에러", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) @PostMapping("/login") + @PreAuthorize("isAnonymous()") public ResponseEntity signIn(@RequestHeader(value = "Authorization", required = false) String accessToken, @RequestBody @Valid SignInReq dto) { if (accessToken != null) throw new GlobalErrorException(ErrorCode.ALREADY_LOGIN_USER); @@ -149,6 +154,7 @@ public ResponseEntity signIn(@RequestHeader(value = "Authorization", required @ApiResponse(responseCode = "4xx", description = "에러", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) @GetMapping("/logout") + @PreAuthorize("isAuthenticated()") public ResponseEntity signOut( @AccessTokenInfo AccessToken accessToken, @CookieValue(value = "refreshToken", required = false) @Valid String refreshToken, diff --git a/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAccountService.java b/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAccountService.java index 5b491170..18d5c2c9 100644 --- a/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAccountService.java +++ b/src/main/java/com/kcy/fitapet/domain/member/service/component/MemberAccountService.java @@ -12,6 +12,7 @@ import com.kcy.fitapet.domain.notification.type.NotificationType; import com.kcy.fitapet.global.common.redis.sms.SmsCertificationService; import com.kcy.fitapet.global.common.redis.sms.SmsPrefix; +import com.kcy.fitapet.global.common.response.code.ErrorCode; import com.kcy.fitapet.global.common.response.code.StatusCode; import com.kcy.fitapet.global.common.response.exception.GlobalErrorException; import lombok.RequiredArgsConstructor; @@ -31,7 +32,7 @@ public class MemberAccountService { private final PasswordEncoder bCryptPasswordEncoder; @Transactional(readOnly = true) - public AccountProfileRes getProfile(Long userId) { + public AccountProfileRes getProfile(Long requestId, Long userId) { Member member = memberSearchService.findById(userId); return AccountProfileRes.from(member); } @@ -42,7 +43,7 @@ public boolean existsUid(String uid) { } @Transactional - public void updateProfile(Long userId, ProfilePatchReq req, MemberAttrType type) { + public void updateProfile(Long requestId, Long userId, ProfilePatchReq req, MemberAttrType type) { Member member = memberSearchService.findById(userId); if (type == MemberAttrType.NAME) { @@ -75,7 +76,7 @@ public void overwritePassword(AccountSearchReq req, String code, SmsPrefix prefi } @Transactional - public void updateNotification(Long userId, NotificationType type) { + public void updateNotification(Long requestId, Long userId, NotificationType type) { Member member = memberSearchService.findById(userId); member.updateNotificationFromType(type); } @@ -118,11 +119,11 @@ private void validatePassword(Member member, String prePassword, String newPassw } /** - * redis에 해당 전화번호에 대한 정보가 저장되어 있는 지 확인하고,
+ * in-memory에 해당 전화번호에 대한 정보가 저장되어 있는 지 확인하고,
* {"phone:"인증번호"} 형태의 인증번호가 일치함을 확인 - * @param phone - * @param code - * @param prefix + * @param phone : 전화번호 + * @param code : 인증번호 + * @param prefix : 인증번호 타입 */ private void validatePhone(String phone, String code, SmsPrefix prefix) { if (!smsCertificationService.existsCertificationNumber(phone, prefix)) { diff --git a/src/main/java/com/kcy/fitapet/domain/oauth2/api/OAuthApi.java b/src/main/java/com/kcy/fitapet/domain/oauth2/api/OAuthApi.java new file mode 100644 index 00000000..ee1851de --- /dev/null +++ b/src/main/java/com/kcy/fitapet/domain/oauth2/api/OAuthApi.java @@ -0,0 +1,15 @@ +package com.kcy.fitapet.domain.oauth2.api; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "OAuth API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth/oauth") +@Slf4j +public class OAuthApi { +} diff --git a/src/main/java/com/kcy/fitapet/global/common/response/SuccessResponse.java b/src/main/java/com/kcy/fitapet/global/common/response/SuccessResponse.java index 8bcd4af3..9b9abf0a 100644 --- a/src/main/java/com/kcy/fitapet/global/common/response/SuccessResponse.java +++ b/src/main/java/com/kcy/fitapet/global/common/response/SuccessResponse.java @@ -20,7 +20,7 @@ public class SuccessResponse { @Schema(description = "응답 상태", defaultValue = "success") private final String status = "success"; - @Schema(description = "응답 코드", example = "200") + @Schema(description = "응답 코드", example = "data or no_content") private Map data; @Builder diff --git a/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtExceptionFilter.java b/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtExceptionFilter.java index da167adc..564f76fb 100644 --- a/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtExceptionFilter.java +++ b/src/main/java/com/kcy/fitapet/global/common/security/filter/JwtExceptionFilter.java @@ -35,7 +35,7 @@ private void sendAuthError(HttpServletResponse response, AuthErrorException e) t response.setContentType("application/json;charset=UTF-8"); response.setStatus(e.getErrorCode().getHttpStatus().value()); - AuthErrorResponse errorResponse = new AuthErrorResponse(e.getErrorCode().name(), e.getErrorCode().getMessage()); + AuthErrorResponse errorResponse = new AuthErrorResponse("error", e.getErrorCode().getMessage()); objectMapper.writeValue(response.getWriter(), errorResponse); } } \ No newline at end of file diff --git a/src/main/java/com/kcy/fitapet/global/common/security/oauth2/OAuth2Provider.java b/src/main/java/com/kcy/fitapet/global/common/security/oauth2/OAuth2Provider.java index 2e5c31be..700d1364 100644 --- a/src/main/java/com/kcy/fitapet/global/common/security/oauth2/OAuth2Provider.java +++ b/src/main/java/com/kcy/fitapet/global/common/security/oauth2/OAuth2Provider.java @@ -1,4 +1,5 @@ package com.kcy.fitapet.global.common.security.oauth2; public interface OAuth2Provider { + } diff --git a/src/main/java/com/kcy/fitapet/global/common/util/jwt/exception/AuthErrorResponse.java b/src/main/java/com/kcy/fitapet/global/common/util/jwt/exception/AuthErrorResponse.java index 5becf854..138d69a0 100644 --- a/src/main/java/com/kcy/fitapet/global/common/util/jwt/exception/AuthErrorResponse.java +++ b/src/main/java/com/kcy/fitapet/global/common/util/jwt/exception/AuthErrorResponse.java @@ -2,6 +2,6 @@ public record AuthErrorResponse(String code, String message) { @Override public String toString() { - return String.format("AuthErrorResponse(code=%s, message=%s)", code, message); + return "AuthErrorResponse(code=" + code + ", message=" + message + ")"; } } \ No newline at end of file diff --git a/src/main/java/com/kcy/fitapet/global/config/security/SecurityConfig.java b/src/main/java/com/kcy/fitapet/global/config/security/SecurityConfig.java index c267d7bb..d76b0bd3 100644 --- a/src/main/java/com/kcy/fitapet/global/config/security/SecurityConfig.java +++ b/src/main/java/com/kcy/fitapet/global/config/security/SecurityConfig.java @@ -29,20 +29,10 @@ public class SecurityConfig { private final JwtSecurityConfig jwtSecurityConfig; - private final String[] publicEndpoints = { - "/api/v1/test", "/api/v1/test/**", + private static final String[] publicReadOnlyPublicEndpoints = { "/favicon.ico", - // Swagger "/api-docs/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger", - - // API - "/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/refresh", - "/api/v1/auth/register-sms/**", "/api/v1/auth/search-sms/**", - "/api/v1/accounts/search", "/api/v1/accounts/search/**", - }; - private static final String[] publicReadOnlyPublicEndpoints = { - "/api/v1/accounts/exists", "/api/v1/accounts/exists/**" }; @Bean @@ -72,9 +62,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests( auth -> auth.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .requestMatchers(HttpMethod.OPTIONS, "*").permitAll() - .requestMatchers(publicEndpoints).permitAll() .requestMatchers(HttpMethod.GET, publicReadOnlyPublicEndpoints).permitAll() - .anyRequest().authenticated() + .anyRequest().permitAll() ) .exceptionHandling( exception -> exception