diff --git a/src/main/java/org/sopt/config/SecurityConfig.java b/src/main/java/org/sopt/config/SecurityConfig.java index a55ec6a..fb50733 100644 --- a/src/main/java/org/sopt/config/SecurityConfig.java +++ b/src/main/java/org/sopt/config/SecurityConfig.java @@ -1,14 +1,20 @@ package org.sopt.config; //API가 인증이 필요한지, permitAll인지 알려주는 config! +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -19,23 +25,40 @@ public class SecurityConfig { private final JwtAuthFilter jwtAuthFilter; + private final ObjectMapper objectMapper; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/login", "/api/v1/auth/reissue").permitAll() - .requestMatchers("/api/v1/users", "/api/v1/users/**").permitAll() + // HTTP Method를 명시해야 의도치 않은 메서드까지 허용되는 보안 위험을 막을 수 있음 - 지현님 코드리뷰 + .requestMatchers(HttpMethod.POST, "/api/v1/auth/login", "/api/v1/auth/reissue").permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .anyRequest().authenticated() ) + .exceptionHandling(exception -> exception.authenticationEntryPoint((request, response, authException) -> { + // 인증 실패 시 Spring 기본 응답 대신 커스텀 JSON 반환 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write("{\"message\": \"인증이 필요합니다.\"}"); + })) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } + @Bean - public BCryptPasswordEncoder passwordEncoder() { + public PasswordEncoder passwordEncoder() { + // 구체 클래스(BCryptPasswordEncoder) 대신 인터페이스(PasswordEncoder) 반환 return new BCryptPasswordEncoder(); } -} \ No newline at end of file +} + +//TODO. CORS 설정해보기 +//TODO. 기찬님의 corsConfigurationSource()를 별도 빈으로 분리하는 구조 참고해서 반영해보기 +//TODO. httpBasic, formLogin 명시적 disable 구현해보기! \ No newline at end of file diff --git a/src/main/java/org/sopt/config/SwaggerConfig.java b/src/main/java/org/sopt/config/SwaggerConfig.java index ce6b4c9..cad77b0 100644 --- a/src/main/java/org/sopt/config/SwaggerConfig.java +++ b/src/main/java/org/sopt/config/SwaggerConfig.java @@ -34,4 +34,6 @@ public OpenAPI openAPI() { .components(new Components().addSecuritySchemes("bearerAuth", securityScheme)) .addSecurityItem(securityRequirement); } -} \ No newline at end of file +} +// TODO. "bearerAuth" 문자열을 상수로 뽑기 (private static final String SECURITY_SCHEME_NAME = "bearerAuth") +// TODO. Info 추가해서 API 제목과 버전 명시하기 \ No newline at end of file diff --git a/src/main/java/org/sopt/controller/AuthController.java b/src/main/java/org/sopt/controller/AuthController.java index 0b35432..e67125a 100644 --- a/src/main/java/org/sopt/controller/AuthController.java +++ b/src/main/java/org/sopt/controller/AuthController.java @@ -80,4 +80,13 @@ public ResponseEntity> logout(Authentication authentication, 그러면 클라가 새 토큰으로 원래 요청을 재시도하게 된다.. 지금은 @RequestParam으로 refresh token을 받게 구현을 해놓았는데 실무에서는 HttpOnly 쿠키로 자동 전송하는 방식을 많이 쓴다고 한다.. -다음 세미나까지 더 찾아봐야지..*/ \ No newline at end of file +다음 세미나까지 더 찾아봐야지..*/ + +//TODO. 로그아웃 응답 코드 204로 변경하기 (이게 더 restful) 한듯 +//TODO. Bearer 토큰 추출 메서드 분리해서 가독성과 재사용성 높이기 +//TODO. Swagger에서 Authorization 헤더를 숨김 처리하기 +//TODO. request.getHeader(HttpHeaders.AUTHORIZATION).substring(...) 에서 헤더가 null이면 NPE가 터지니까 null체크나, @RequestHeader 로 받자 +//TODO. Refresh Token을 HttpOnly 쿠키로 관리해보자 +//TODO. CookieUtil로 쿠키 생성 로직 분리해보자 +//TODO. AuthSuccessCode.SIGN_UP_SUCCESS, LOCAL_LOGIN_SUCCESS 등 성공 응답도 코드로 관리해보기 +//TODO. 회원가입과 로그인이 분리 — 내 기존코드는 회원가입(/users)과 로그인(/auth/login)이 분리되어 있는데, 기찬님 코드는 /auth/signup으로 회원가입도 인증 도메인에서 처리하며 가입 즉시 토큰을 발급해준다. 이게 더 자연스러운 흐름인 것 같다 \ No newline at end of file diff --git a/src/main/java/org/sopt/controller/LikeController.java b/src/main/java/org/sopt/controller/LikeController.java index 3ab5b55..fb1cc54 100644 --- a/src/main/java/org/sopt/controller/LikeController.java +++ b/src/main/java/org/sopt/controller/LikeController.java @@ -50,4 +50,8 @@ public ResponseEntity> deleteLike( likeService.deleteLike(userId, postId); return ResponseEntity.ok(BaseResponse.success(null)); } -} \ No newline at end of file +} + +//TODO. @RequestMapping 경로 통합 — 클래스 레벨에 /api/v1/posts/{postId}/likes로 올리면 메서드마다 반복 안 해도 되니까..! +//TODO. Long.parseLong(authentication.getName()) 중복 제거 +//TODO. 좋아요 추가 응답을 201로 변경 \ No newline at end of file diff --git a/src/main/java/org/sopt/controller/PostController.java b/src/main/java/org/sopt/controller/PostController.java index b612e2c..2b69241 100644 --- a/src/main/java/org/sopt/controller/PostController.java +++ b/src/main/java/org/sopt/controller/PostController.java @@ -87,4 +87,6 @@ public ResponseEntity deletePost(@PathVariable Long id) { postService.deletePost(id); return ResponseEntity.noContent().build(); } -} \ No newline at end of file +} + +//TODO. updatePost, deletePost에 Authentication 추가하기 \ No newline at end of file diff --git a/src/main/java/org/sopt/controller/UserController.java b/src/main/java/org/sopt/controller/UserController.java index b9ceae2..60da1c1 100644 --- a/src/main/java/org/sopt/controller/UserController.java +++ b/src/main/java/org/sopt/controller/UserController.java @@ -61,4 +61,7 @@ public ResponseEntity> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.ok(BaseResponse.success("유저 삭제 성공", null)); } -} \ No newline at end of file +} + +//TODO. deleteUser에 Authentication 추가하기 +//TODO. updateUser 기능 추가해보기 \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/LoggedOutToken.java b/src/main/java/org/sopt/domain/LoggedOutToken.java index 7c83ed6..29d0876 100644 --- a/src/main/java/org/sopt/domain/LoggedOutToken.java +++ b/src/main/java/org/sopt/domain/LoggedOutToken.java @@ -28,4 +28,6 @@ public static LoggedOutToken of(String token, long expiresInSeconds) { public String getToken() { return token; } public LocalDateTime getExpiresAt() { return expiresAt; } -} \ No newline at end of file +} +//TODO. token 컬럼에 length = 500 추가하고 +//TODO. BlacklistedAccessToken 요걸로 이름 바꾸장 \ No newline at end of file diff --git a/src/main/java/org/sopt/domain/User.java b/src/main/java/org/sopt/domain/User.java index ac5906f..14a9d58 100644 --- a/src/main/java/org/sopt/domain/User.java +++ b/src/main/java/org/sopt/domain/User.java @@ -22,3 +22,6 @@ public User(String nickname, String email, String password) { public String getEmail() { return this.email; } public String getPassword() { return this.password; } } + +// TODO.소프트 딜리트(@SQLDelete + @SQLRestriction) 패턴 적용해보기 +// TODO. 룸복 사용해보자 \ No newline at end of file diff --git a/src/main/java/org/sopt/exception/AuthErrorCode.java b/src/main/java/org/sopt/exception/AuthErrorCode.java new file mode 100644 index 0000000..18522b3 --- /dev/null +++ b/src/main/java/org/sopt/exception/AuthErrorCode.java @@ -0,0 +1,4 @@ +package org.sopt.exception; + +public class AuthErrorCode { +} diff --git a/src/main/java/org/sopt/exception/ErrorCode.java b/src/main/java/org/sopt/exception/ErrorCode.java index 4b5bd4c..cd74579 100644 --- a/src/main/java/org/sopt/exception/ErrorCode.java +++ b/src/main/java/org/sopt/exception/ErrorCode.java @@ -9,7 +9,7 @@ import org.springframework.http.HttpStatus; public enum ErrorCode { //400 BadRequest - INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "잘못된 입려값입니다."), + INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."), TITLE_TOO_LONG("POST_002", HttpStatus.BAD_REQUEST, "게시글 제목은 50자를 초과할 수 없습니다."), CONTENT_REQUIRED("POST_003", HttpStatus.BAD_REQUEST, "게시글 내용은 필수입니다."), @@ -43,4 +43,7 @@ public String getMessage(){ public HttpStatus getHttpStatus(){ return httpStatus; } -} \ No newline at end of file +} + +//TODO. ErrorCode를 인터페이스로 분리하고 도메인별 enum으로 나누기 +//TODO. 인증 관련 에러 코드 추가하기 \ No newline at end of file diff --git a/src/main/java/org/sopt/exception/PostErrorCode.java b/src/main/java/org/sopt/exception/PostErrorCode.java new file mode 100644 index 0000000..a49c447 --- /dev/null +++ b/src/main/java/org/sopt/exception/PostErrorCode.java @@ -0,0 +1,4 @@ +package org.sopt.exception; + +public class PostErrorCode { +} diff --git a/src/main/java/org/sopt/exception/UserErrorCode.java b/src/main/java/org/sopt/exception/UserErrorCode.java new file mode 100644 index 0000000..3624182 --- /dev/null +++ b/src/main/java/org/sopt/exception/UserErrorCode.java @@ -0,0 +1,4 @@ +package org.sopt.exception; + +public class UserErrorCode { +} diff --git a/src/main/java/org/sopt/service/AuthService.java b/src/main/java/org/sopt/service/AuthService.java index 5fb61f7..39a4fda 100644 --- a/src/main/java/org/sopt/service/AuthService.java +++ b/src/main/java/org/sopt/service/AuthService.java @@ -138,4 +138,13 @@ public MemberResponse getMemberById(Long memberId) { 다시 로그인한다!! (403으로 내려주도록 했었는데, 이걸 쓰다보니 401이 더 맞는 것 같아서 바꾸러..가봅니다..) -*/ \ No newline at end of file +*/ + +// TODO. IllegalArgumentException 대신 커스텀 예외 사용하기 +//TODO.reissue 순서 변경 — JWT 검증을 DB 조회보다 먼저 하는 게 더 효율적이라고 한다 +//TODO.logout에 중복 체크 추가 — existsByToken 확인 후 저장하기 +//TODO.getMemberById의 불필요한 캐스팅 제거 — (Member) +//TODO.BCryptPasswordEncoder → PasswordEncoder 인터페이스로 바꾸자 +//TODO. reissue에서 memberId 일치 여부 검증해보자 +//TODO. logout에서 Access Token 블랙리스트 저장해보자 +//TODO. jwtService.getExpiresAt(accessToken)으로 정확한 만료 시각 저장해보기 \ No newline at end of file diff --git a/src/main/java/org/sopt/service/UserService.java b/src/main/java/org/sopt/service/UserService.java index 0c920d3..6841f12 100644 --- a/src/main/java/org/sopt/service/UserService.java +++ b/src/main/java/org/sopt/service/UserService.java @@ -36,4 +36,9 @@ public UserResponse getUser(Long id) { public void deleteUser(Long id) { userRepository.deleteById(id); } -} \ No newline at end of file +} + +//TODO.이메일 중복 검사 추가 +//TODO.deleteUser에 존재 여부 확인 추가 — deleteById 대신 findById 후 없으면 예외를 던지도록 바꾸기 +//TODO.updateUser 구현하기 +//TODO.BCryptPasswordEncoder → PasswordEncoder 인터페이스로 바꾸기 \ No newline at end of file