Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public ResponseEntity<DeleteRatePlanResponse> deleteRatePlan(String ratePlanId)
return ResponseEntity.ok(response);
}

// 요금제 지표 조회
@GetMapping("/api/admin/rateplans/metrics")
public ResponseEntity<RatePlanMetricsResponse> getRatePlanMetrics(
@RequestParam(defaultValue = "1") int page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/ureca/ufit/entity/RatePlan.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

// 비회원 회원 모두 JWT검증 필요X
private static final List<String> WHITE_LIST = List.of(
"/error", "/favicon.ico", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html",
"/api/auth/login",
Expand All @@ -46,7 +45,6 @@ public class JwtFilter extends OncePerRequestFilter {
"/actuator/info"
);

// 비회원이면 JWT검증 필요X, 회원이면 JWT검증 필요
private static final List<String> PUBLIC_LIST = List.of(
"/api/chats/message",
"/api/chats/review",
Expand All @@ -66,10 +64,8 @@ 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);
Expand All @@ -78,19 +74,16 @@ 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public Authentication attemptAuthentication(HttpServletRequest request,

Authentication authentication = new UsernamePasswordAuthenticationToken(email, password);

// 인증 메니저에게 인증 객체 위임
return this.getAuthenticationManager().authenticate(authentication);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,18 @@ 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ 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);
Expand All @@ -46,27 +45,25 @@ 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)
)
);

} catch (RestApiException e) {
// 쿠키나 레디스에서 리프레시 토큰을 찾지 못했을 경우 정상처리

if(e.getErrorCode().equals(CommonErrorCode.REFRESH_NOT_FOUND))
return;
try {
Expand All @@ -79,7 +76,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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/com/ureca/ufit/global/auth/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -117,10 +117,8 @@ 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@
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",
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ public class ErrorResponseDto {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<ValidationError> errors;

/**
* @Valid를 사용했을 때 에러가 발생한 경우 어느 필드에서 에러가 발생했는지 응답을 위한 ValidationError를 내부 정적 클래스
*/
@Getter
@Builder
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,13 @@ protected ResponseEntity<Object> 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 = "잘못된 요청입니다.";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,23 @@ public ProfanityFilter(List<String> 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)<script.*?>.*?</script.*?>"),
Pattern.compile("(?i)(onerror|onload|alert|prompt|confirm)\\s*="),
Pattern.compile("(?i)<.*?javascript:.*?>"),
Pattern.compile("(?i)<iframe.*?>.*?</iframe>"),

// 기타 명령어
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<BanwordFilterPolicy> policies) {
if (input == null || policies.isEmpty()) {
return input;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ void tearDown() {
@DisplayName("요금제의 판매 상태를 변경한다.")
@Test
void updateRatePlanSalesStatus() throws Exception {
// given

RatePlan savedRatePlan = ratePlanRepository.save(RatePlanFixture.ratePlan("판매 중인 요금제", true, false));

// when // then
mockMvc.perform(patch("/api/admin/rateplans/{ratePlanId}", savedRatePlan.getId())
.contentType(APPLICATION_JSON)
.header("Authorization", accessTokenOfAdmin)
Expand All @@ -47,7 +46,7 @@ void updateRatePlanSalesStatus() throws Exception {
@DisplayName("상품의 ID값이 없으면 판매 상태를 변경할 수 없다.")
@Test
void throwValidationExceptionWhenRatePlanIdIsNull() throws Exception {
// when // then

mockMvc.perform(patch("/api/admin/rateplans/{ratePlanId}", " ")
.contentType(APPLICATION_JSON)
.header("Authorization", accessTokenOfAdmin)
Expand Down
Loading