Skip to content

Commit ccca620

Browse files
authored
Merge pull request #17 from Geumpumta/wifi
feat : 교내 Wi-Fi 검증을 위한 서버 측 IP 추출 및 및 Nginx 프록시 대응
2 parents 2409065 + f324c44 commit ccca620

File tree

8 files changed

+98
-37
lines changed

8 files changed

+98
-37
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.gpt.geumpumtabackend.global.wifi;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
5+
public class IpUtil {
6+
7+
public static String getClientIp(HttpServletRequest request) {
8+
String ip = request.getHeader("X-Real-IP");
9+
10+
if(isUnknown(ip)) {
11+
String xForwardedFor = request.getHeader("X-Forwarded-For");
12+
if(!isUnknown(xForwardedFor)) {
13+
String[] candidates =xForwardedFor.split(",");
14+
for (int i = candidates.length - 1; i >= 0; i--) {
15+
String candidate = candidates[i].trim();
16+
if (!isUnknown(candidate)) {
17+
ip = candidate;
18+
break;
19+
}
20+
}
21+
}
22+
}
23+
24+
if(isUnknown(ip)){
25+
ip = request.getRemoteAddr();
26+
}
27+
28+
if (ip != null && ip.contains(",")) {
29+
ip = ip.split(",")[0].trim();
30+
}
31+
32+
return ip;
33+
}
34+
35+
private static boolean isUnknown(String ip) {
36+
return ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip);
37+
}
38+
}

src/main/java/com/gpt/geumpumtabackend/study/api/StudySessionApi.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.swagger.v3.oas.annotations.media.Schema;
1818
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1919
import io.swagger.v3.oas.annotations.tags.Tag;
20+
import jakarta.servlet.http.HttpServletRequest;
2021
import jakarta.validation.Valid;
2122
import org.springframework.http.ResponseEntity;
2223
import org.springframework.security.access.prepost.PreAuthorize;
@@ -103,7 +104,8 @@ ResponseEntity<ResponseBody<StudySessionResponse>> getTodayStudySession(
103104
@PreAuthorize("isAuthenticated() and hasRole('USER')")
104105
ResponseEntity<ResponseBody<StudyStartResponse>> startStudySession(
105106
@Valid @RequestBody StudyStartRequest request,
106-
@Parameter(hidden = true) Long userId
107+
@Parameter(hidden = true) Long userId,
108+
HttpServletRequest httpServletRequest
107109
);
108110

109111
@Operation(
@@ -168,6 +170,6 @@ ResponseEntity<ResponseBody<Void>> endStudySession(
168170
@PreAuthorize("isAuthenticated() and hasRole('USER')")
169171
ResponseEntity<ResponseBody<Void>> processHeartBeat(
170172
@Valid @RequestBody HeartBeatRequest heartBeatRequest,
171-
@Parameter(hidden = true) Long userId
172-
);
173+
@Parameter(hidden = true) Long userId,
174+
HttpServletRequest httpServletRequest);
173175
}

src/main/java/com/gpt/geumpumtabackend/study/controller/StudySessionController.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.gpt.geumpumtabackend.study.dto.response.StudySessionResponse;
1111
import com.gpt.geumpumtabackend.study.dto.response.StudyStartResponse;
1212
import com.gpt.geumpumtabackend.study.service.StudySessionService;
13+
import jakarta.servlet.http.HttpServletRequest;
1314
import jakarta.validation.Valid;
1415
import lombok.RequiredArgsConstructor;
1516
import lombok.extern.slf4j.Slf4j;
@@ -42,8 +43,10 @@ public ResponseEntity<ResponseBody<StudySessionResponse>> getTodayStudySession(L
4243
@PostMapping("/start")
4344
@PreAuthorize("isAuthenticated() and hasRole('USER')")
4445
@AssignUserId
45-
public ResponseEntity<ResponseBody<StudyStartResponse>> startStudySession(@Valid @RequestBody StudyStartRequest request, Long userId){
46-
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(studySessionService.startStudySession(request, userId)));
46+
public ResponseEntity<ResponseBody<StudyStartResponse>> startStudySession(@Valid @RequestBody StudyStartRequest request,
47+
Long userId,
48+
HttpServletRequest httpServletRequest){
49+
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(studySessionService.startStudySession(request, userId, httpServletRequest)));
4750
}
4851

4952
/*
@@ -63,8 +66,8 @@ public ResponseEntity<ResponseBody<Void>> endStudySession(@Valid @RequestBody St
6366
@PostMapping("/heart-beat")
6467
@PreAuthorize("isAuthenticated() and hasRole('USER')")
6568
@AssignUserId
66-
public ResponseEntity<ResponseBody<Void>> processHeartBeat(@Valid @RequestBody HeartBeatRequest heartBeatRequest, Long userId){
67-
studySessionService.updateHeartBeat(heartBeatRequest, userId);
69+
public ResponseEntity<ResponseBody<Void>> processHeartBeat(@Valid @RequestBody HeartBeatRequest heartBeatRequest, Long userId, HttpServletRequest httpServletRequest){
70+
studySessionService.updateHeartBeat(heartBeatRequest, userId, httpServletRequest);
6871
return ResponseEntity.ok(ResponseUtil.createSuccessResponse());
6972
}
7073
}

src/main/java/com/gpt/geumpumtabackend/study/dto/request/HeartBeatRequest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ public record HeartBeatRequest(
99

1010
@NotBlank(message = "SSID는 필수입니다")
1111
String ssid,
12-
13-
String bssid, // 선택적 (null 가능)
14-
15-
@NotBlank(message = "IP 주소는 필수입니다")
16-
String ipAddress
12+
13+
@NotBlank(message = "BSSID는 필수입니다")
14+
String bssid
15+
1716
) {
1817

1918
}

src/main/java/com/gpt/geumpumtabackend/study/dto/request/StudyStartRequest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ public record StudyStartRequest(
1111

1212
@NotBlank(message = "SSID는 필수입니다")
1313
String ssid,
14-
15-
String bssid, // 선택적 (null 가능)
16-
17-
@NotBlank(message = "IP 주소는 필수입니다")
18-
String ipAddress
19-
){
2014

15+
@NotBlank(message = "BSSID는 필수입니다")
16+
String bssid
17+
18+
){
2119
}
2220

2321

src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.gpt.geumpumtabackend.global.exception.BusinessException;
44
import com.gpt.geumpumtabackend.global.exception.ExceptionType;
5+
import com.gpt.geumpumtabackend.global.wifi.IpUtil;
56
import com.gpt.geumpumtabackend.study.domain.StudySession;
67
import com.gpt.geumpumtabackend.study.dto.request.HeartBeatRequest;
78
import com.gpt.geumpumtabackend.study.dto.request.StudyEndRequest;
@@ -14,6 +15,7 @@
1415
import com.gpt.geumpumtabackend.wifi.dto.WiFiValidationResult;
1516
import com.gpt.geumpumtabackend.wifi.service.CampusWiFiValidationService;
1617

18+
import jakarta.servlet.http.HttpServletRequest;
1719
import lombok.RequiredArgsConstructor;
1820
import lombok.extern.slf4j.Slf4j;
1921
import org.springframework.stereotype.Service;
@@ -45,10 +47,10 @@ public StudySessionResponse getTodayStudySession(Long userId) {
4547
공부 시작
4648
*/
4749
@Transactional
48-
public StudyStartResponse startStudySession(StudyStartRequest request, Long userId) {
50+
public StudyStartResponse startStudySession(StudyStartRequest request, Long userId, HttpServletRequest httpServletRequest) {
4951
// Wi-Fi 검증
5052
WiFiValidationResult validationResult = wifiValidationService.validateFromCache(
51-
request.ssid(), request.bssid(), request.ipAddress()
53+
request.ssid(), request.bssid(), httpServletRequest
5254
);
5355

5456
if (!validationResult.isValid()) {
@@ -81,12 +83,12 @@ public void endStudySession(StudyEndRequest request, Long userId) {
8183
하트비트 처리
8284
*/
8385
@Transactional
84-
public void updateHeartBeat(HeartBeatRequest heartBeatRequest, Long userId) {
86+
public void updateHeartBeat(HeartBeatRequest heartBeatRequest, Long userId, HttpServletRequest httpServletRequest) {
8587
Long sessionId = heartBeatRequest.sessionId();
8688

8789
// Wi-Fi 검증 (캐시 우선 사용)
8890
WiFiValidationResult validationResult = wifiValidationService.validateFromCache(
89-
heartBeatRequest.ssid(), heartBeatRequest.bssid(), heartBeatRequest.ipAddress()
91+
heartBeatRequest.ssid(), heartBeatRequest.bssid(), httpServletRequest
9092
);
9193

9294
if (!validationResult.isValid()) {

src/main/java/com/gpt/geumpumtabackend/wifi/service/CampusWiFiValidationService.java

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.gpt.geumpumtabackend.wifi.service;
22

3+
import com.gpt.geumpumtabackend.global.wifi.IpUtil;
34
import com.gpt.geumpumtabackend.wifi.config.CampusWiFiProperties;
45
import com.gpt.geumpumtabackend.wifi.dto.WiFiValidationResult;
6+
import jakarta.servlet.http.HttpServletRequest;
57
import lombok.RequiredArgsConstructor;
68
import lombok.extern.slf4j.Slf4j;
79
import org.springframework.data.redis.core.RedisTemplate;
@@ -23,9 +25,13 @@ public class CampusWiFiValidationService {
2325
private static final String WIFI_CACHE_KEY_PREFIX = "campus_wifi_validation:";
2426

2527

26-
public WiFiValidationResult validateCampusWiFi(String ssid, String bssid, String ipAddress) {
28+
public WiFiValidationResult validateCampusWiFi(String ssid, String bssid, HttpServletRequest request) {
2729

2830
try {
31+
// 서버에서 클라이언트 IP 추출
32+
String ipAddress = IpUtil.getClientIp(request);
33+
log.info("Wi-Fi validation request - SSID: {}, BSSID: {}, IP: {}", ssid, bssid, ipAddress);
34+
2935
// 캠퍼스 내부인지 확인
3036
boolean isInCampus = isInCampusNetwork(ssid, bssid, ipAddress);
3137

@@ -38,33 +44,46 @@ public WiFiValidationResult validateCampusWiFi(String ssid, String bssid, String
3844
}
3945

4046
} catch (Exception e) {
47+
log.error("Wi-Fi validation error", e);
4148
return WiFiValidationResult.error("Wi-Fi 검증 중 오류가 발생했습니다: " + e.getMessage());
4249
}
4350
}
4451

4552

46-
public WiFiValidationResult validateFromCache(String ssid, String bssid, String ipAddress) {
47-
48-
// IP 주소와 SSID를 통해 키를 생성 후 Redis에서 조회
49-
String cacheKey = buildCacheKey(ssid, ipAddress);
50-
Boolean cachedResult = (Boolean) redisTemplate.opsForValue().get(cacheKey);
51-
52-
if (cachedResult != null) {
53-
return cachedResult
54-
? WiFiValidationResult.valid("캠퍼스 네트워크입니다 (캐시)")
55-
: WiFiValidationResult.invalid("캠퍼스 네트워크가 아닙니다 (캐시)");
53+
public WiFiValidationResult validateFromCache(String ssid, String bssid, HttpServletRequest request) {
54+
try {
55+
// 서버에서 클라이언트 IP 추출
56+
String ipAddress = IpUtil.getClientIp(request);
57+
log.info("Wi-Fi validation cache request - SSID: {}, BSSID: {}, IP: {}", ssid, bssid, ipAddress);
58+
// IP 주소와 SSID를 통해 키를 생성 후 Redis에서 조회
59+
String cacheKey = buildCacheKey(ssid, ipAddress);
60+
Boolean cachedResult = (Boolean) redisTemplate.opsForValue().get(cacheKey);
61+
62+
if (cachedResult != null) {
63+
log.debug("Wi-Fi validation cache hit - SSID: {}, IP: {}, Result: {}", ssid, ipAddress, cachedResult);
64+
return cachedResult
65+
? WiFiValidationResult.valid("캠퍼스 네트워크입니다 (캐시)")
66+
: WiFiValidationResult.invalid("캠퍼스 네트워크가 아닙니다 (캐시)");
5667
}
57-
// 캐시에 없으면 전체 검증 수행
58-
return validateCampusWiFi(ssid, bssid, ipAddress);
68+
69+
// 캐시에 없으면 전체 검증 수행
70+
log.debug("Wi-Fi validation cache miss - performing full validation");
71+
return validateCampusWiFi(ssid, bssid, request);
72+
73+
} catch (Exception e) {
74+
log.error("Wi-Fi cache validation error", e);
75+
return WiFiValidationResult.error("Wi-Fi 검증 중 오류가 발생했습니다: " + e.getMessage());
76+
}
5977
}
6078

6179

6280
private boolean isInCampusNetwork(String ssid, String bssid, String ipAddress) {
81+
82+
// 설정 파일 Wi-fi 목록 불러오기
6383
List<CampusWiFiProperties.WiFiNetwork> activeNetworks = wifiProperties.networks()
6484
.stream()
6585
.filter(CampusWiFiProperties.WiFiNetwork::active)
6686
.toList();
67-
6887
for (CampusWiFiProperties.WiFiNetwork network : activeNetworks) {
6988
// 1. SSID 체크
7089
if (!network.isValidSSID(ssid)) {

src/main/resources/application-local.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ spring:
2727

2828
jpa:
2929
hibernate:
30-
ddl-auto: create
30+
ddl-auto: update
3131
open-in-view: false
3232
show-sql: true
3333
properties:

0 commit comments

Comments
 (0)