Merge pull request 'feat: OpenSky OAuth2 인증 + 수집 주기 5분 조정' (#73) from feat/opensky-oauth2-credits into develop
This commit is contained in:
커밋
b6456145d5
@ -3,6 +3,7 @@ package gc.mda.kcg.collector.aircraft;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import gc.mda.kcg.collector.CollectorStatusTracker;
|
||||
import gc.mda.kcg.config.AppProperties;
|
||||
import gc.mda.kcg.domain.aircraft.AircraftDto;
|
||||
import gc.mda.kcg.domain.aircraft.AircraftPosition;
|
||||
import gc.mda.kcg.domain.aircraft.AircraftPositionRepository;
|
||||
@ -11,9 +12,15 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
@ -24,22 +31,23 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class OpenSkyCollector {
|
||||
|
||||
private static final String BASE_URL = "https://opensky-network.org/api";
|
||||
|
||||
// 이란/중동 bbox
|
||||
private static final String IRAN_PARAMS = "lamin=24&lomin=30&lamax=42&lomax=62";
|
||||
// 한국/동아시아 bbox
|
||||
private static final String KOREA_PARAMS = "lamin=20&lomin=115&lamax=45&lomax=145";
|
||||
private static final long TOKEN_REFRESH_MARGIN_SEC = 120;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AircraftCacheStore cacheStore;
|
||||
private final AircraftPositionRepository positionRepository;
|
||||
private final CollectorStatusTracker tracker;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||
|
||||
@Scheduled(initialDelay = 30_000, fixedDelay = 60_000)
|
||||
private String accessToken;
|
||||
private Instant tokenExpiresAt = Instant.EPOCH;
|
||||
|
||||
@Scheduled(initialDelay = 30_000, fixedDelay = 300_000)
|
||||
public void collectIran() {
|
||||
List<AircraftDto> aircraft = fetchStates(IRAN_PARAMS);
|
||||
if (!aircraft.isEmpty()) {
|
||||
@ -48,12 +56,12 @@ public class OpenSkyCollector {
|
||||
persistAll(aircraft, "opensky", "iran");
|
||||
tracker.recordSuccess("opensky-iran", "iran", aircraft.size());
|
||||
} else {
|
||||
tracker.recordFailure("opensky-iran", "iran", "빈 응답 또는 429");
|
||||
tracker.recordFailure("opensky-iran", "iran", "빈 응답 또는 인증 실패");
|
||||
}
|
||||
log.debug("OpenSky 이란 수집 완료: {} 항공기", aircraft.size());
|
||||
}
|
||||
|
||||
@Scheduled(initialDelay = 45_000, fixedDelay = 60_000)
|
||||
@Scheduled(initialDelay = 180_000, fixedDelay = 300_000)
|
||||
public void collectKorea() {
|
||||
List<AircraftDto> aircraft = fetchStates(KOREA_PARAMS);
|
||||
if (!aircraft.isEmpty()) {
|
||||
@ -62,15 +70,23 @@ public class OpenSkyCollector {
|
||||
persistAll(aircraft, "opensky", "korea");
|
||||
tracker.recordSuccess("opensky-korea", "korea", aircraft.size());
|
||||
} else {
|
||||
tracker.recordFailure("opensky-korea", "korea", "빈 응답 또는 429");
|
||||
tracker.recordFailure("opensky-korea", "korea", "빈 응답 또는 인증 실패");
|
||||
}
|
||||
log.debug("OpenSky 한국 수집 완료: {} 항공기", aircraft.size());
|
||||
}
|
||||
|
||||
private List<AircraftDto> fetchStates(String bboxParams) {
|
||||
try {
|
||||
String url = BASE_URL + "/states/all?" + bboxParams;
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
String token = getAccessToken();
|
||||
String url = appProperties.getCollector().getOpenSkyBaseUrl() + "/states/all?" + bboxParams;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
if (token != null) {
|
||||
headers.setBearerAuth(token);
|
||||
}
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
|
||||
if (response.getStatusCode().value() == 429) {
|
||||
log.warn("OpenSky 429 rate limited, 스킵");
|
||||
return List.of();
|
||||
@ -83,6 +99,47 @@ public class OpenSkyCollector {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized String getAccessToken() {
|
||||
if (accessToken != null && Instant.now().isBefore(tokenExpiresAt.minusSeconds(TOKEN_REFRESH_MARGIN_SEC))) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
String clientId = appProperties.getCollector().getOpenSkyClientId();
|
||||
String clientSecret = appProperties.getCollector().getOpenSkyClientSecret();
|
||||
if (clientId == null || clientSecret == null) {
|
||||
log.debug("OpenSky OAuth2 미설정, 익명 모드로 동작");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String authUrl = appProperties.getCollector().getOpenSkyAuthUrl();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("grant_type", "client_credentials");
|
||||
body.add("client_id", clientId);
|
||||
body.add("client_secret", clientSecret);
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(authUrl, request, String.class);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.getBody());
|
||||
accessToken = json.get("access_token").asText();
|
||||
int expiresIn = json.get("expires_in").asInt();
|
||||
tokenExpiresAt = Instant.now().plusSeconds(expiresIn);
|
||||
|
||||
log.info("OpenSky OAuth2 토큰 발급 완료 (만료: {}초)", expiresIn);
|
||||
return accessToken;
|
||||
} catch (Exception e) {
|
||||
log.warn("OpenSky OAuth2 토큰 발급 실패: {}, 익명 모드로 폴백", e.getMessage());
|
||||
accessToken = null;
|
||||
tokenExpiresAt = Instant.EPOCH;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistAll(List<AircraftDto> aircraft, String source, String region) {
|
||||
try {
|
||||
Instant now = Instant.now();
|
||||
|
||||
@ -40,6 +40,9 @@ public class AppProperties {
|
||||
public static class Collector {
|
||||
private String airplanesLiveBaseUrl = "https://api.airplanes.live/v2";
|
||||
private String openSkyBaseUrl = "https://opensky-network.org/api";
|
||||
private String openSkyAuthUrl = "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token";
|
||||
private String openSkyClientId;
|
||||
private String openSkyClientSecret;
|
||||
private String gdeltBaseUrl = "https://api.gdeltproject.org/api/v2/doc/doc";
|
||||
private String googleNewsBaseUrl = "https://news.google.com/rss/search";
|
||||
private String celestrakBaseUrl = "https://celestrak.org/NORAD/elements/gp.php";
|
||||
|
||||
@ -11,3 +11,6 @@ app:
|
||||
client-id: YOUR_GOOGLE_CLIENT_ID
|
||||
auth:
|
||||
allowed-domain: gcsc.co.kr
|
||||
collector:
|
||||
open-sky-client-id: YOUR_OPENSKY_CLIENT_ID
|
||||
open-sky-client-secret: YOUR_OPENSKY_CLIENT_SECRET
|
||||
|
||||
@ -11,5 +11,8 @@ app:
|
||||
client-id: ${GOOGLE_CLIENT_ID}
|
||||
auth:
|
||||
allowed-domain: ${AUTH_ALLOWED_DOMAIN:gcsc.co.kr}
|
||||
collector:
|
||||
open-sky-client-id: ${OPENSKY_CLIENT_ID:}
|
||||
open-sky-client-secret: ${OPENSKY_CLIENT_SECRET:}
|
||||
cors:
|
||||
allowed-origins: http://localhost:5173,https://kcg.gc-si.dev
|
||||
|
||||
@ -4,6 +4,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 추가
|
||||
- OpenSky OAuth2 Client Credentials 인증 (토큰 자동 갱신)
|
||||
|
||||
### 변경
|
||||
- OpenSky 수집 주기 60초 → 300초 (일일 크레딧 소비 11,520 → 2,304)
|
||||
|
||||
## [2026-03-19]
|
||||
|
||||
### 변경
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user