Skip to content

Commit 3347c9e

Browse files
committed
fix(gps-monitoring): GPS 스푸핑 탐지 시스템 최적화 및 처리 개선
1 parent e351f76 commit 3347c9e

File tree

10 files changed

+564
-189
lines changed

10 files changed

+564
-189
lines changed

src/main/java/back/fcz/domain/capsule/repository/CapsuleOpenLogRepository.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.data.repository.query.Param;
99
import org.springframework.stereotype.Repository;
1010

11+
import java.time.LocalDateTime;
1112
import java.util.List;
1213
import java.util.Optional;
1314

@@ -32,6 +33,20 @@ List<CapsuleOpenLog> findTop15ByCapsuleId_CapsuleIdAndIpAddressOrderByOpenedAtDe
3233
String ipAddress
3334
);
3435

36+
// 특정 시간 이후의 로그만 조회 (회원용)
37+
List<CapsuleOpenLog> findTop15ByCapsuleId_CapsuleIdAndMemberIdAndOpenedAtAfterOrderByOpenedAtDesc(
38+
Long capsuleId,
39+
Long memberId,
40+
LocalDateTime after
41+
);
42+
43+
// 특정 시간 이후의 로그만 조회 (비회원용)
44+
List<CapsuleOpenLog> findTop15ByCapsuleId_CapsuleIdAndIpAddressAndOpenedAtAfterOrderByOpenedAtDesc(
45+
Long capsuleId,
46+
String ipAddress,
47+
LocalDateTime after
48+
);
49+
3550
// 성공 기록 확인 (회원용)
3651
boolean existsByCapsuleId_CapsuleIdAndMemberIdAndStatus(
3752
Long capsuleId,

src/main/java/back/fcz/domain/capsule/service/CapsuleReadService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ private CapsuleOpenLog createOpenLog(
593593
.viewerType(viewerType)
594594
.status(status)
595595
.anomalyType(AnomalyType.NONE)
596-
.openedAt(requestDto.unlockAt())
596+
.openedAt(requestDto.serverTime())
597597
.currentLat(requestDto.locationLat())
598598
.currentLng(requestDto.locationLng())
599599
.userAgent(requestDto.userAgent())

src/main/java/back/fcz/domain/sanction/constant/SanctionConstants.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ public int getScoreByAnomaly(AnomalyType anomalyType) {
5959
return score;
6060
}
6161

62+
public int getLogWindowHours() {
63+
if (sanctionProperties.getAnomalyDetection() == null) {
64+
log.warn("anomaly-detection 설정이 없습니다. 기본값 24를 반환합니다.");
65+
return 24;
66+
}
67+
return sanctionProperties.getAnomalyDetection().getLogWindowHours();
68+
}
69+
70+
public int getDuplicateRequestSeconds() {
71+
if (sanctionProperties.getAnomalyDetection() == null) {
72+
log.warn("anomaly-detection 설정이 없습니다. 기본값 3을 반환합니다.");
73+
return 3;
74+
}
75+
return sanctionProperties.getAnomalyDetection().getDuplicateRequestSeconds();
76+
}
77+
6278
// 자동 제재 사유 생성
6379
public static String buildAutoSanctionReason(String detail) {
6480
return AUTO_SANCTION_REASON_PREFIX + detail;

src/main/java/back/fcz/domain/sanction/properties/SanctionProperties.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class SanctionProperties {
1616

1717
private RateLimit rateLimit = new RateLimit();
1818
private Monitoring monitoring = new Monitoring();
19+
private AnomalyDetection anomalyDetection = new AnomalyDetection();
1920

2021
@Getter
2122
@Setter
@@ -39,4 +40,11 @@ public static class Thresholds {
3940
private int limit;
4041
}
4142
}
43+
44+
@Getter
45+
@Setter
46+
public static class AnomalyDetection {
47+
private int logWindowHours;
48+
private int duplicateRequestSeconds;
49+
}
4250
}

src/main/java/back/fcz/domain/sanction/util/AnomalyDetector.java

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ public class AnomalyDetector {
1111
// 지구 반지름 (km)
1212
private static final double EARTH_RADIUS_KM = 6371.0;
1313

14+
// GPS 오차 범위 상수화
15+
private static final double GPS_ERROR_MARGIN_KM = 0.1; // 100m: 일반 GPS 오차
16+
private static final double GPS_RECONNECTION_ERROR_KM = 0.3; // 300m: GPS 재연결 오차
17+
private static final double SHORT_MOVEMENT_MARGIN_KM = 0.3; // 300m: 짧은 시간 이동 허용
18+
1419
// 3단계 속도 임계값
1520
private static final double EXTREME_SPEED_KMH = 1000.0; // 즉시 차단
1621
private static final double HIGH_SPEED_KMH = 300.0; // 강한 의심
1722
private static final double SUSPICIOUS_SPEED_KMH = 150.0; // 누적 의심
1823

19-
// 시간 간격 기준 (초)
20-
private static final long SHORT_INTERVAL_SEC = 300; // 5분
21-
private static final long MEDIUM_INTERVAL_SEC = 3600; // 1시간
22-
2324
// 시간 동기화 허용 오차 (분)
2425
private static final int TIME_SYNC_TOLERANCE_MINUTES = 10;
2526

@@ -49,11 +50,7 @@ public static double calculateDistance(double lat1, double lng1, double lat2, do
4950
*/
5051
public static double calculateSpeed(double distance, long timeDiffSeconds) {
5152
if (timeDiffSeconds <= 0) {
52-
if (distance >= 0.001) { // 1m 이상 이동
53-
return Double.MAX_VALUE;
54-
}
55-
56-
return 0.0;
53+
return -1.0;
5754
}
5855

5956
double hours = timeDiffSeconds / 3600.0;
@@ -68,14 +65,23 @@ public static double calculateSpeed(double distance, long timeDiffSeconds) {
6865
public static int classifyMovementAnomaly(
6966
double previousLat, double previousLng,
7067
double currentLat, double currentLng,
71-
LocalDateTime previousTime, LocalDateTime currentTime) {
68+
LocalDateTime previousTime, LocalDateTime currentTime,
69+
int duplicateRequestThresholdSeconds) {
7270
long timeDiffSeconds = Duration.between(previousTime, currentTime).getSeconds();
7371
double distance = calculateDistance(previousLat, previousLng, currentLat, currentLng);
7472

75-
// 시간 역행 (클라이언트 조작)
73+
log.debug("이동 분석 - 시간차: {}초, 거리: {}km, 이전시각: {}, 현재시각: {}",
74+
timeDiffSeconds, distance, previousTime, currentTime);
75+
76+
// === 1단계: 중복 요청 필터링 ===
77+
if (timeDiffSeconds >= 0 && timeDiffSeconds < duplicateRequestThresholdSeconds) {
78+
log.debug("중복 요청 감지 ({}초 이내): GPS 오차 무시", timeDiffSeconds);
79+
return 0;
80+
}
81+
82+
// === 2단계: 시간 역행 검증 ===
7683
if (timeDiffSeconds < 0) {
77-
// GPS 오차 범위(100m) 내는 허용
78-
if (distance < 0.1) {
84+
if (distance < GPS_ERROR_MARGIN_KM) {
7985
log.debug("시간 역행이지만 GPS 오차 범위 내: {}초, {}km", timeDiffSeconds, distance);
8086
return 0;
8187
}
@@ -84,10 +90,10 @@ public static int classifyMovementAnomaly(
8490
return 3;
8591
}
8692

87-
// 정확히 동일 시간 (0초)
93+
// === 3단계: 동일 시간 검증 ===
8894
if (timeDiffSeconds == 0) {
89-
// 200m 미만은 GPS 재연결 오차로 간주
90-
if (distance < 0.2) {
95+
// 300m 미만은 GPS 재연결 오차로 간주
96+
if (distance < GPS_RECONNECTION_ERROR_KM) {
9197
log.debug("동일 시간이지만 GPS 재연결 오차 범위 내: {}km", distance);
9298
return 0;
9399
}
@@ -96,41 +102,34 @@ public static int classifyMovementAnomaly(
96102
return 3;
97103
}
98104

99-
// 일반 GPS 오차(5-10m) + 실내 오차(~50m) 고려
100-
if (distance < 0.1) { // 100m
105+
// === 4단계: GPS 오차 범위 검증 ===
106+
if (distance < GPS_ERROR_MARGIN_KM) { // 100m
101107
log.debug("이동 거리 {}km는 GPS 오차 범위 내로 판단", distance);
102108
return 0;
103109
}
104110

105-
// 짧은 시간 + 짧은 거리는 추가로 관대하게
106-
if (timeDiffSeconds < 60 && distance < 0.2) { // 1분 & 200m
111+
// === 5단계: 짧은 시간 + 짧은 거리 추가 관대 처리 ===
112+
if (timeDiffSeconds < 60 && distance < SHORT_MOVEMENT_MARGIN_KM) { // 1분 & 300m
107113
log.debug("짧은 시간 이동({}초, {}km)은 정상으로 판단", timeDiffSeconds, distance);
108114
return 0;
109115
}
110116

117+
// === 6단계: 속도 계산 및 분류 ===
111118
double speed = calculateSpeed(distance, timeDiffSeconds);
112119

113-
// 시간 간격에 따라 임계값 조정
114-
double adjustedExtremeThreshold = EXTREME_SPEED_KMH;
115-
double adjustedHighThreshold = HIGH_SPEED_KMH;
116-
double adjustedSuspiciousThreshold = SUSPICIOUS_SPEED_KMH;
117-
118-
if (timeDiffSeconds < 300) { // 5분 미만
119-
adjustedSuspiciousThreshold = 80.0;
120-
} else if (timeDiffSeconds < 600) { // 10분 미만
121-
adjustedSuspiciousThreshold = 100.0;
122-
} else if (timeDiffSeconds < 1800) { // 30분 미만
123-
adjustedSuspiciousThreshold = 120.0;
124-
} else if (timeDiffSeconds < 3600) { // 1시간 미만
125-
adjustedSuspiciousThreshold = 140.0;
126-
} else { // 1시간 이상
127-
adjustedSuspiciousThreshold = 150.0;
120+
if (speed < 0) {
121+
log.warn("속도 계산 불가: 시간차={}초, 거리={}km", timeDiffSeconds, distance);
122+
return 0;
128123
}
129124

130-
if (speed >= adjustedExtremeThreshold) {
125+
double adjustedSuspiciousThreshold = calculateAdjustedThreshold(timeDiffSeconds);
126+
127+
log.debug("계산된 속도: {}km/h (조정된 의심 임계값: {}km/h)", speed, adjustedSuspiciousThreshold);
128+
129+
if (speed >= EXTREME_SPEED_KMH) {
131130
log.warn("즉시 차단급 이동 감지: {}km/h", speed);
132131
return 3;
133-
} else if (speed >= adjustedHighThreshold) {
132+
} else if (speed >= HIGH_SPEED_KMH) {
134133
log.warn("강한 의심 이동 감지: {}km/h", speed);
135134
return 2;
136135
} else if (speed >= adjustedSuspiciousThreshold) {
@@ -141,6 +140,21 @@ public static int classifyMovementAnomaly(
141140
return 0;
142141
}
143142

143+
// 시간 간격에 따른 임계값 계산
144+
private static double calculateAdjustedThreshold(long timeDiffSeconds) {
145+
if (timeDiffSeconds < 300) { // 5분 미만
146+
return 80.0;
147+
} else if (timeDiffSeconds < 600) { // 10분 미만
148+
return 100.0;
149+
} else if (timeDiffSeconds < 1800) { // 30분 미만
150+
return 120.0;
151+
} else if (timeDiffSeconds < 3600) { // 1시간 미만
152+
return 140.0;
153+
} else { // 1시간 이상
154+
return 150.0;
155+
}
156+
}
157+
144158
/**
145159
* 서버 시간과 클라이언트 시간의 차이가 허용 범위 내인지 검증
146160
* 클라이언트가 시간을 조작했을 가능성을 감지
@@ -153,7 +167,12 @@ public static boolean isTimeManipulation(LocalDateTime serverTime, LocalDateTime
153167

154168
long diffMinutes = Math.abs(Duration.between(serverTime, clientTime).toMinutes());
155169

156-
return diffMinutes > TIME_SYNC_TOLERANCE_MINUTES;
170+
if (diffMinutes > TIME_SYNC_TOLERANCE_MINUTES) {
171+
log.warn("시간 조작 의심: 서버-클라이언트 시간차 = {}분", diffMinutes);
172+
return true;
173+
}
174+
175+
return false;
157176
}
158177

159178
/**
@@ -165,8 +184,13 @@ public static boolean isValidCoordinate(Double latitude, Double longitude) {
165184
return false;
166185
}
167186

168-
// 위도: -90 ~ 90, 경도: -180 ~ 180
169-
return latitude >= -90.0 && latitude <= 90.0
187+
boolean isValid = latitude >= -90.0 && latitude <= 90.0
170188
&& longitude >= -180.0 && longitude <= 180.0;
189+
190+
if (!isValid) {
191+
log.warn("유효하지 않은 좌표: lat={}, lng={}", latitude, longitude);
192+
}
193+
194+
return isValid;
171195
}
172196
}

0 commit comments

Comments
 (0)