diff --git a/src/main/java/com/ureca/ufit/domain/admin/controller/AdminController.java b/src/main/java/com/ureca/ufit/domain/admin/controller/AdminController.java index 28acfa0..9a9840a 100644 --- a/src/main/java/com/ureca/ufit/domain/admin/controller/AdminController.java +++ b/src/main/java/com/ureca/ufit/domain/admin/controller/AdminController.java @@ -44,7 +44,6 @@ public ResponseEntity deleteRatePlan(String ratePlanId) return ResponseEntity.ok(response); } - // 요금제 지표 조회 @GetMapping("/api/admin/rateplans/metrics") public ResponseEntity getRatePlanMetrics( @RequestParam(defaultValue = "1") int page, diff --git a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java index 4290da0..115d834 100644 --- a/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java +++ b/src/main/java/com/ureca/ufit/domain/chatbot/service/ChatBotReviewService.java @@ -74,8 +74,7 @@ private QuestionSummaryDto requestUserQuerySummary(CreateChatBotReviewRequest re CreateUserQuerySummaryRequest chatReviewSummaryRequest = new CreateUserQuerySummaryRequest( request.recommendationMessageId()); - // QuestionSummaryDto questionSummaryDto = restTemplate.postForObject(url, chatReviewSummaryRequest, - // QuestionSummaryDto.class); + RestClient restClient = RestClient.create(); QuestionSummaryDto questionSummaryDto = restClient.post() diff --git a/src/main/java/com/ureca/ufit/domain/user/service/UserService.java b/src/main/java/com/ureca/ufit/domain/user/service/UserService.java index 9a74c1f..48b49ad 100644 --- a/src/main/java/com/ureca/ufit/domain/user/service/UserService.java +++ b/src/main/java/com/ureca/ufit/domain/user/service/UserService.java @@ -18,7 +18,6 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - //DB 확인용 회원가입 @Transactional public RegisterResponse register(RegisterRequest request) { String encodedPassword = passwordEncoder.encode(request.password()); diff --git a/src/main/java/com/ureca/ufit/entity/RatePlan.java b/src/main/java/com/ureca/ufit/entity/RatePlan.java index 2c01fc6..81be657 100644 --- a/src/main/java/com/ureca/ufit/entity/RatePlan.java +++ b/src/main/java/com/ureca/ufit/entity/RatePlan.java @@ -118,9 +118,9 @@ public static RatePlan of(String planName, String summary, int monthlyFee, int d .dataAllowance(dataAllowance) .voiceAllowance(voiceAllowance) .smsAllowance(smsAllowance) - .basicBenefit(basicBenefit) // not null - .specialBenefit(specialBenefit) // null 가능 - .discountBenefit(discountBenefit) // null 가능.. + .basicBenefit(basicBenefit) + .specialBenefit(specialBenefit) + .discountBenefit(discountBenefit) .isEnabled(true) .isDeleted(false) .build(); diff --git a/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java b/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java index b44560f..24c0505 100644 --- a/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java +++ b/src/main/java/com/ureca/ufit/global/auth/filter/JwtFilter.java @@ -32,7 +32,7 @@ @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { - // 비회원 회원 모두 JWT검증 필요X + private static final List WHITE_LIST = List.of( "/error", "/favicon.ico", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/api/auth/login", @@ -46,7 +46,7 @@ public class JwtFilter extends OncePerRequestFilter { "/actuator/info" ); - // 비회원이면 JWT검증 필요X, 회원이면 JWT검증 필요 + private static final List PUBLIC_LIST = List.of( "/api/chats/message", "/api/chats/review", @@ -66,10 +66,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse boolean isPublic = PUBLIC_LIST.stream().anyMatch(pub -> matcher.match(pub, request.getRequestURI())); - // 어세스 토큰 유효성 검사 시작 + String bearerToken = request.getHeader(AUTH_HEADER); - // 비회원일 때 검증 로직 + if (Optional.ofNullable(bearerToken).isEmpty()) { if (!isPublic) { throw new RestApiException(CommonErrorCode.NOT_EXIST_BEARER_SUFFIX); @@ -78,19 +78,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } - // 어세스 토큰 추출 + String accessToken = Optional.of(bearerToken) .filter(token -> token.startsWith(BEARER_PREFIX)) .map(token -> token.substring(BEARER_PREFIX.length())) .orElseThrow(() -> new RestApiException(CommonErrorCode.NOT_EXIST_BEARER_SUFFIX)); - // 어세스 토큰 검증, 블랙 리스트 확인 + JwtUtil.validateAccessToken(accessToken, secretKey); if (Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + accessToken))) { throw new RestApiException(CommonErrorCode.INVALID_TOKEN); } - // 인증 객체를 설정하고 시큐리티 홀더에 저장 + String email = JwtUtil.getEmail(accessToken, secretKey); UserDetails userDetails = userDetailsService.loadUserByUsername(email); Authentication authentication = new UsernamePasswordAuthenticationToken( diff --git a/src/main/java/com/ureca/ufit/global/auth/filter/LoginFilter.java b/src/main/java/com/ureca/ufit/global/auth/filter/LoginFilter.java index 1157e2d..cf4b779 100644 --- a/src/main/java/com/ureca/ufit/global/auth/filter/LoginFilter.java +++ b/src/main/java/com/ureca/ufit/global/auth/filter/LoginFilter.java @@ -33,7 +33,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, Authentication authentication = new UsernamePasswordAuthenticationToken(email, password); - // 인증 메니저에게 인증 객체 위임 + return this.getAuthenticationManager().authenticate(authentication); } } diff --git a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLoginSuccessHandler.java b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLoginSuccessHandler.java index 58c361d..f80d19a 100644 --- a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLoginSuccessHandler.java +++ b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLoginSuccessHandler.java @@ -33,24 +33,24 @@ public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler { public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - // 로그인 성공 시 인증 객체의 principal을 정의하기 위한 유저 정보 + CustomUserDetails userDetails = (CustomUserDetails)authentication.getPrincipal(); - // 로그인 성공 시 어세스/리프레시 토큰 발급 + String accessToken = JwtUtil.createAccessToken(userDetails.email(), secretKeyKey); String refreshToken = JwtUtil.createRefreshToken(userDetails.email(), secretKeyKey); - // 레디스에 리프레시 토큰 저장 + RefreshToken refreshTokenEntity = RefreshToken.of(refreshToken, userDetails.email()); refreshTokenRepository.save(refreshTokenEntity); - // 쿠키에 리프레시 토큰 저장 (timeout 3일) + JwtUtil.updateRefreshTokenCookie(response, refreshToken, REFRESH_TOKEN_EXPIRED_MS / 1000); - // 헤더에 어세스 토큰 저장 + response.setHeader(AUTH_HEADER, BEARER_PREFIX + accessToken); - // 바디에 Login Response 저장 + LoginResponse loginResponse = LoginResponse.of(userDetails.getUsername(), userDetails.role()); objectMapper.writeValue(response.getWriter(), loginResponse); } diff --git a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java index 39afeca..e2304d0 100644 --- a/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java +++ b/src/main/java/com/ureca/ufit/global/auth/handler/CustomLogoutHandler.java @@ -36,7 +36,7 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { try { - // 로그아웃 시 헤더에 있는 어세스 토큰 검증 + String bearerToken = request.getHeader(AUTH_HEADER); if (bearerToken == null || !bearerToken.startsWith(BEARER_PREFIX)) { throw new RestApiException(CommonErrorCode.NOT_EXIST_BEARER_SUFFIX); @@ -46,19 +46,18 @@ public void logout(HttpServletRequest request, HttpServletResponse response, try { JwtUtil.validateAccessToken(accessToken, secretKey); - // 블랙 리스트에 어세스 토큰 추가 + addToBlacklistRedis(accessToken); } catch (RestApiException e) { - // 어세스토큰 만료는 정상 처리 if( !e.getErrorCode().equals(CommonErrorCode.EXPIRED_TOKEN)) throw e; } String refreshToken = JwtUtil.getRefreshTokenCookies(request); - // 쿠키에서 리프레시 토큰 삭제 (timeout을 0으로 두어 즉시 삭제) + JwtUtil.updateRefreshTokenCookie(response, null, 0); - // Redis에서 해당 리프레시 토큰 키 삭제 + refreshTokenRepository.delete( refreshTokenRepository.findById(refreshToken).orElseThrow( () -> new RestApiException(CommonErrorCode.REFRESH_NOT_FOUND) @@ -66,7 +65,6 @@ public void logout(HttpServletRequest request, HttpServletResponse response, ); } catch (RestApiException e) { - // 쿠키나 레디스에서 리프레시 토큰을 찾지 못했을 경우 정상처리 if(e.getErrorCode().equals(CommonErrorCode.REFRESH_NOT_FOUND)) return; try { @@ -79,7 +77,7 @@ public void logout(HttpServletRequest request, HttpServletResponse response, private void addToBlacklistRedis(String accessToken) { Date expiration = JwtUtil.getExpiration(accessToken, secretKey); - long ttl = expiration.getTime() - System.currentTimeMillis(); // TTL: 남은 시간 - 현재 시간 + long ttl = expiration.getTime() - System.currentTimeMillis(); redisTemplate.opsForValue().set(BLACKLIST_PREFIX + accessToken, "logout", ttl, TimeUnit.MILLISECONDS); } } diff --git a/src/main/java/com/ureca/ufit/global/auth/provider/LoginProvider.java b/src/main/java/com/ureca/ufit/global/auth/provider/LoginProvider.java index abd39a5..f0017a8 100644 --- a/src/main/java/com/ureca/ufit/global/auth/provider/LoginProvider.java +++ b/src/main/java/com/ureca/ufit/global/auth/provider/LoginProvider.java @@ -27,10 +27,8 @@ public Authentication authenticate(Authentication authentication) String email = authentication.getName(); - // 인증 객체의 principal을 정의하기 위한 유저 정보 UserDetails userDetails = userDetailsService.loadUserByUsername(email); - // 비밀번호 검증 String password = authentication.getCredentials().toString(); if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new RestApiException(UserErrorCode.USER_PASSWORD_MISMATCH); diff --git a/src/main/java/com/ureca/ufit/global/auth/service/AuthService.java b/src/main/java/com/ureca/ufit/global/auth/service/AuthService.java index 5be72f4..1f2dad5 100644 --- a/src/main/java/com/ureca/ufit/global/auth/service/AuthService.java +++ b/src/main/java/com/ureca/ufit/global/auth/service/AuthService.java @@ -26,31 +26,24 @@ public class AuthService { public void reissueToken(String bearerToken, String refreshToken, HttpServletResponse response) { - // 어세스 토큰 추출 String accessToken = bearerToken.substring(BEARER_PREFIX.length()); - // 블랙 리스트에 어세스 토큰이 있는 지 확인 if (redisTemplate.hasKey(BLACKLIST_PREFIX + accessToken)) { throw new RestApiException(CommonErrorCode.INVALID_TOKEN); } - // 리프레시 토큰이 레디스에 있는지 확인 RefreshToken refreshTokenEntity = refreshTokenRepository.findById(refreshToken).orElseThrow(() -> new RestApiException(CommonErrorCode.REFRESH_NOT_FOUND) ); - // 어세스 토큰이 만료되 었는지 검증하고 이메일 추출 String email = JwtUtil.getEmailOnlyIfExpired(accessToken, secretKey); - // 어세스 토큰과 리프레시 토큰의 매핑 검증 if (!email.equals(refreshTokenEntity.getEmail())) { throw new RestApiException(CommonErrorCode.REFRESH_DENIED); } - // 리프레시 토큰 만료 여부 검증 JwtUtil.validateRefreshToken(refreshToken, secretKey); - // 리프레시 토큰 폐기 후 어세스 토큰과 리프레시 토큰 재발급 (RTR) String newRefreshToken = JwtUtil.createRefreshToken(email, secretKey); RefreshToken newRefreshTokenEntity = RefreshToken.of(newRefreshToken, email); refreshTokenRepository.deleteById(refreshToken); diff --git a/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java b/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java index 9d6e017..7d60fb3 100644 --- a/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java +++ b/src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java @@ -42,8 +42,8 @@ public class JwtUtil { public static final String COOKIE_HEADER_NAME = "Set-Cookie"; public static final String COOKIE_SAME_SITE_STRATEGY = "Lax"; - public static final int ACCESS_TOKEN_EXPIRED_MS = 1000 * 60 * 30; // 30분 - public static final int REFRESH_TOKEN_EXPIRED_MS = 1000 * 60 * 60 * 24 * 3; // 3일 + public static final int ACCESS_TOKEN_EXPIRED_MS = 1000 * 60 * 30; + public static final int REFRESH_TOKEN_EXPIRED_MS = 1000 * 60 * 60 * 24 * 3; public static String createToken(String email, String type, SecretKey secretKey, long expiresIn) { return Jwts.builder() @@ -117,10 +117,9 @@ private static void validateToken(String token, SecretKey secretKey, String expe } } - // 토큰 검증 및 만료된 토큰에서 사용자의 이메일을 추출(주의: 토큰 재발급 로직에서만 사용할 것!) + public static String getEmailOnlyIfExpired(String token, SecretKey secretKey) { try { - // 만료되지 않았다면 재발급 대상이 아님 → 예외 발생 parseClaims(token, secretKey); throw new RestApiException(CommonErrorCode.REFRESH_DENIED); } catch (RestApiException e) { diff --git a/src/main/java/com/ureca/ufit/global/config/SecurityConfig.java b/src/main/java/com/ureca/ufit/global/config/SecurityConfig.java index 9b494e5..8aae15d 100644 --- a/src/main/java/com/ureca/ufit/global/config/SecurityConfig.java +++ b/src/main/java/com/ureca/ufit/global/config/SecurityConfig.java @@ -31,15 +31,13 @@ @Configuration @RequiredArgsConstructor -public class SecurityConfig { +public class SecurityConfig { private static final String[] USER_AUTH_LIST = { - // 인증이 필요한 API 패턴 "/api/chats/{chatroomId:\\d+}" }; private static final String[] ADMIN_AUTH_LIST = { - // 관리자 인증이 필요한 API 패턴 "/api/admin/rateplans", "/api/admin/rateplans/{rateplanId:\\d+}", "/api/admin/rateplans/metrics", @@ -82,9 +80,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http - .addFilterBefore(exceptionHandlerFilter, UsernamePasswordAuthenticationFilter.class) // ← 예외 핸들러 - .addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class) // ← 로그인 필터 - .addFilterBefore(jwtFilter, BasicAuthenticationFilter.class); // ← JWT 필터 + .addFilterBefore(exceptionHandlerFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtFilter, BasicAuthenticationFilter.class); http .authorizeHttpRequests(auth -> auth @@ -104,7 +102,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } - // Spring Security cors Bean 등록 @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); diff --git a/src/main/java/com/ureca/ufit/global/exception/CommonErrorCode.java b/src/main/java/com/ureca/ufit/global/exception/CommonErrorCode.java index eca4a2f..dea08a3 100644 --- a/src/main/java/com/ureca/ufit/global/exception/CommonErrorCode.java +++ b/src/main/java/com/ureca/ufit/global/exception/CommonErrorCode.java @@ -12,7 +12,6 @@ public enum CommonErrorCode implements ErrorCode { RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), - // JWT 관련 에러코드 INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid token"), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired token"), NOT_EXIST_BEARER_SUFFIX(HttpStatus.BAD_REQUEST, "Bearer prefix is missing."), diff --git a/src/main/java/com/ureca/ufit/global/exception/ErrorResponseDto.java b/src/main/java/com/ureca/ufit/global/exception/ErrorResponseDto.java index c87b49e..d9320e0 100644 --- a/src/main/java/com/ureca/ufit/global/exception/ErrorResponseDto.java +++ b/src/main/java/com/ureca/ufit/global/exception/ErrorResponseDto.java @@ -21,9 +21,7 @@ public class ErrorResponseDto { @JsonInclude(JsonInclude.Include.NON_EMPTY) private final List errors; - /** - * @Valid를 사용했을 때 에러가 발생한 경우 어느 필드에서 에러가 발생했는지 응답을 위한 ValidationError를 내부 정적 클래스 - */ + @Getter @Builder @RequiredArgsConstructor diff --git a/src/main/java/com/ureca/ufit/global/exception/GlobalExceptionHandler.java b/src/main/java/com/ureca/ufit/global/exception/GlobalExceptionHandler.java index efb9847..a09710a 100644 --- a/src/main/java/com/ureca/ufit/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/ureca/ufit/global/exception/GlobalExceptionHandler.java @@ -74,16 +74,13 @@ protected ResponseEntity handleMethodArgumentNotValid( @NonNull WebRequest request) { log.warn(ex.getMessage(), ex); - // 필드 오류 우선 확인 String errMessage; if (!ex.getBindingResult().getFieldErrors().isEmpty()) { errMessage = ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); } - // 클래스 레벨(Global) 오류 확인 else if (!ex.getBindingResult().getGlobalErrors().isEmpty()) { errMessage = ex.getBindingResult().getGlobalErrors().get(0).getDefaultMessage(); } - // 기본 메시지 else { errMessage = "잘못된 요청입니다."; } diff --git a/src/main/java/com/ureca/ufit/global/profanity/BanwordFilterPolicy.java b/src/main/java/com/ureca/ufit/global/profanity/BanwordFilterPolicy.java index 64bdfc5..a930053 100644 --- a/src/main/java/com/ureca/ufit/global/profanity/BanwordFilterPolicy.java +++ b/src/main/java/com/ureca/ufit/global/profanity/BanwordFilterPolicy.java @@ -1,9 +1,9 @@ package com.ureca.ufit.global.profanity; public enum BanwordFilterPolicy { - NUMBERS("[\\p{N}]"), // 모든 숫자 - WHITESPACES("[\\s]"), // 공백 문자 - FOREIGN("[\\p{L}&&[^ㄱ-ㅎ가-힣ㅏ-ㅣa-zA-Z]]"); // 한글/영문 제외 문자 (특수문자) + NUMBERS("[\\p{N}]"), + WHITESPACES("[\\s]"), + FOREIGN("[\\p{L}&&[^ㄱ-ㅎ가-힣ㅏ-ㅣa-zA-Z]]"); private final String regex; diff --git a/src/main/java/com/ureca/ufit/global/profanity/ProfanityFilter.java b/src/main/java/com/ureca/ufit/global/profanity/ProfanityFilter.java index 05bda90..e3c35f5 100644 --- a/src/main/java/com/ureca/ufit/global/profanity/ProfanityFilter.java +++ b/src/main/java/com/ureca/ufit/global/profanity/ProfanityFilter.java @@ -20,26 +20,23 @@ public ProfanityFilter(List bannedWords) { .build(); this.attackPatterns = List.of( - //SQL Injection Pattern.compile("(?i)(\\bor\\b|\\band\\b)?\\s*['\"].*?['\"]\\s*(=|>|<|like)?\\s*['\"].*?['\"]"), Pattern.compile("(?i)(union(.*?)select)"), Pattern.compile("(?i)(drop|insert|delete|update|select|truncate|alter)\\s+\\w+"), Pattern.compile("(?i)(--)|(#)"), - // XSS Pattern.compile("(?i).*?"), Pattern.compile("(?i)(onerror|onload|alert|prompt|confirm)\\s*="), Pattern.compile("(?i)<.*?javascript:.*?>"), Pattern.compile("(?i).*?"), - // 기타 명령어 Pattern.compile("(?i)exec\\s+(xp_cmdshell|sp_)"), Pattern.compile("(?i)benchmark\\s*\\(.*?\\)"), Pattern.compile("(?i)sleep\\s*\\(\\s*[0-9]+\\s*\\)") ); } - // 정제된 데이터 반환 + public String normalize(String input, Set policies) { if (input == null || policies.isEmpty()) { return input;