增加了用户认证接口

This commit is contained in:
Tian jianyong 2024-12-24 09:58:56 +08:00
parent 15374cc01e
commit cfcdaac3e5
10 changed files with 257 additions and 22 deletions

View File

@ -42,6 +42,10 @@ if(NOT APPLE)
# CentOS 使 Boost 1.69
set(BOOST_ROOT "/usr/include/boost169")
set(BOOST_LIBRARYDIR "/usr/lib64/boost169")
else()
# macOS 使 Homebrew Boost
set(BOOST_ROOT "/opt/homebrew/Cellar/boost/1.87.0")
set(BOOST_LIBRARYDIR "/opt/homebrew/Cellar/boost/1.87.0/lib")
endif()
find_package(Boost 1.69.0 REQUIRED COMPONENTS system filesystem thread)

View File

@ -18,12 +18,17 @@
{"point": "T8", "longitude": 120.08676664, "latitude": 36.35004529},
{"point": "T9", "longitude": 120.08520616, "latitude": 36.34964473},
{"point": "T10", "longitude": 120.08710569, "latitude": 36.34917893},
{"point": "T11", "longitude": 120.0873865, "latitude": 36.3509885}
{"point": "T11", "longitude": 120.0873865, "latitude": 36.3509885},
{"point": "T12", "longitude": 120.08603613, "latitude": 36.35190217},
{"point": "T13", "longitude": 120.08509148, "latitude": 36.35041247}
]
},
"data_source": {
"host": "localhost",
"port": 8081,
"username": "dianxin",
"password": "dianxin@123",
"auth_path": "/api/login",
"aircraft_path": "/api/getCurrentFlightPositions",
"vehicle_path": "/api/getCurrentVehiclePositions",
"traffic_light_path": "/api/getTrafficLightSignals",

View File

@ -1,14 +1,27 @@
# 位置数据接口对接方案
# 数据接口对接方案
## 第1章 位置数据接口对接方案
### 1.1 航空器位置数据接入
### 1.1 登录认证
1. 登录接口:<http://IP:端口/login>
2. 请求方式post
3. 参数username、password
4. 示例:<http://127.0.0.1:8080/login?username=XXXX&password=XXXX>
5、返回值 data 为返回的鉴权token后续接口需要再header中携带data所有的数据是一个token不要截断
示例:{
    "status": 200,
    "msg": "登入成功",
    "data": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY"
}
### 1.2 航空器位置数据接入
数据来源:接入并转发从空管接收到的融合数据
1. 接口地址:<http://IP:端口/openApi/getCurrentFlightPositions>
2. 请求方式get
2. 请求方式get,需要在 Header 中携带认证信息,字段名为 Authorization值为认证接口返回的token
3. 返回格式:以 JSON 格式返回数据一次请求返回List集合对象
@ -23,13 +36,13 @@
| 5 | altitude | 海拔高度 | double | 否 |
| 6 | trackNumber | 航迹号 | String | 否 |
### 1.2 车辆位置数据接入
### 1.3 车辆位置数据接入
数据来源:仅传递目前机场已接入的车辆位置数据
1. 接口地址:<http://IP:端口/openApi/getCurrentVehiclePositions>
2. 请求方式get
2. 请求方式get,需要在 Header 中携带认证信息,字段名为 Authorization值为认证接口返回的token
3. 返回格式:以 JSON 格式返回数据一次请求返回List集合对象

View File

@ -9,6 +9,9 @@ struct DataSourceConfig {
std::string aircraft_path;
std::string vehicle_path;
std::string traffic_light_path;
std::string auth_path; // 认证接口路径
std::string username; // 用户名
std::string password; // 密码
int refresh_interval_ms;
int timeout_ms; // 连接超时时间, 单位: 毫秒
int read_timeout_ms; // 读取超时时间, 单位: 毫秒, 小于连接超时时间

View File

@ -51,6 +51,9 @@ public:
std::string aircraft_path;
std::string vehicle_path;
std::string traffic_light_path;
std::string auth_path; // 认证接口路径
std::string username; // 用户名
std::string password; // 密码
int refresh_interval_ms;
int timeout_ms;
int read_timeout_ms;
@ -210,6 +213,9 @@ inline void to_json(json& j, const SystemConfig::DataSource& p) {
{"aircraft_path", p.aircraft_path},
{"vehicle_path", p.vehicle_path},
{"traffic_light_path", p.traffic_light_path},
{"auth_path", p.auth_path},
{"username", p.username},
{"password", p.password},
{"refresh_interval_ms", p.refresh_interval_ms},
{"timeout_ms", p.timeout_ms},
{"read_timeout_ms", p.read_timeout_ms}
@ -222,6 +228,9 @@ inline void from_json(const json& j, SystemConfig::DataSource& p) {
j.at("aircraft_path").get_to(p.aircraft_path);
j.at("vehicle_path").get_to(p.vehicle_path);
j.at("traffic_light_path").get_to(p.traffic_light_path);
j.at("auth_path").get_to(p.auth_path);
j.at("username").get_to(p.username);
j.at("password").get_to(p.password);
j.at("refresh_interval_ms").get_to(p.refresh_interval_ms);
j.at("timeout_ms").get_to(p.timeout_ms);
j.at("read_timeout_ms").get_to(p.read_timeout_ms);

View File

@ -69,8 +69,12 @@ bool System::initialize() {
system_config.data_source.aircraft_path,
system_config.data_source.vehicle_path,
system_config.data_source.traffic_light_path,
system_config.data_source.auth_path,
system_config.data_source.username,
system_config.data_source.password,
system_config.data_source.refresh_interval_ms,
system_config.data_source.timeout_ms
system_config.data_source.timeout_ms,
system_config.data_source.read_timeout_ms
};
WarnConfig warnConfig{

View File

@ -14,7 +14,8 @@ HTTPDataSource::HTTPDataSource(const DataSourceConfig& config)
: config_(config)
, host_(config.host)
, port_(std::to_string(config.port))
, last_health_check_(std::chrono::steady_clock::now()) {
, last_health_check_(std::chrono::steady_clock::now())
, is_authenticated_(false) {
curl_ = curl_easy_init();
if (!curl_) {
Logger::error("Failed to initialize CURL");
@ -36,7 +37,9 @@ bool HTTPDataSource::connect() {
return false;
}
}
return true;
// 进行认证
return authenticate();
}
void HTTPDataSource::disconnect() {
@ -115,12 +118,20 @@ bool HTTPDataSource::ensureConnected() {
return tryReconnect();
}
bool HTTPDataSource::sendRequest(const std::string& path, std::string& response) {
bool HTTPDataSource::sendRequest(const std::string& path, std::string& response, HttpMethod method) {
if (!curl_) {
Logger::error("CURL not initialized");
return false;
}
// 检查认证状态
if (!is_authenticated_) {
Logger::error("Not authenticated");
if (!authenticate()) {
return false;
}
}
std::string url = "http://" + host_ + ":" + port_ + path;
Logger::debug("Sending request to: ", url);
@ -132,10 +143,24 @@ bool HTTPDataSource::sendRequest(const std::string& path, std::string& response)
curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, config_.timeout_ms / 1000);
curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1L);
// 根据方法设置不同的选项
if (method == HttpMethod::GET) {
curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L);
} else if (method == HttpMethod::POST) {
curl_easy_setopt(curl_, CURLOPT_POST, 1L);
}
// 添加 HTTP 头
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Accept: application/json");
headers = curl_slist_append(headers, "Cache-Control: no-cache");
// 添加认证 token
if (!auth_token_.empty()) {
std::string auth_header = "Authorization: " + auth_token_;
headers = curl_slist_append(headers, auth_header.c_str());
}
curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl_);
@ -148,6 +173,17 @@ bool HTTPDataSource::sendRequest(const std::string& path, std::string& response)
long http_code = 0;
curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code);
// 如果返回 401尝试重新认证
if (http_code == 401) {
Logger::warning("Token expired, trying to re-authenticate");
if (authenticate()) {
// 重新发送请求
return sendRequest(path, response, method);
}
return false;
}
if (http_code != 200) {
Logger::error("HTTP request failed with code: ", http_code);
return false;
@ -164,7 +200,7 @@ bool HTTPDataSource::fetchAircraftData(std::vector<Aircraft>& aircraft) {
}
std::string response;
if (!sendRequest(config_.aircraft_path, response)) {
if (!sendRequest(config_.aircraft_path, response, HttpMethod::GET)) {
return false;
}
@ -179,7 +215,7 @@ bool HTTPDataSource::fetchVehicleData(std::vector<Vehicle>& vehicles) {
}
std::string response;
if (!sendRequest(config_.vehicle_path, response)) {
if (!sendRequest(config_.vehicle_path, response, HttpMethod::GET)) {
return false;
}
@ -194,7 +230,7 @@ bool HTTPDataSource::fetchTrafficLightSignals(std::vector<TrafficLightSignal>& s
}
std::string response;
if (!sendRequest(config_.traffic_light_path, response)) {
if (!sendRequest(config_.traffic_light_path, response, HttpMethod::GET)) {
return false;
}
@ -363,4 +399,86 @@ bool HTTPDataSource::parseAircraftResponse(const std::string& response, std::vec
Logger::error("Error processing aircraft response: ", e.what());
return false;
}
}
bool HTTPDataSource::authenticate() {
std::lock_guard<std::mutex> lock(auth_mutex_);
if (!curl_) {
Logger::error("CURL not initialized");
return false;
}
// 构造认证请求URL
std::string url = "http://" + host_ + ":" + port_ + config_.auth_path;
// 构造认证请求体
nlohmann::json request = {
{"username", config_.username},
{"password", config_.password}
};
std::string request_body = request.dump();
std::string response;
Logger::debug("Sending authentication request with username: ", config_.username);
Logger::debug("Request body: ", request_body);
// 设置CURL选项
curl_easy_reset(curl_);
curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl_, CURLOPT_POST, 1L);
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, request_body.c_str());
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, static_cast<long>(request_body.length()));
curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response);
// 设置请求头
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers);
// 发送请求
CURLcode res = curl_easy_perform(curl_);
curl_slist_free_all(headers);
if (res != CURLE_OK) {
Logger::error("Failed to authenticate: ", curl_easy_strerror(res));
return false;
}
// 检查响应状态码
long http_code = 0;
curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) {
Logger::error("Authentication failed with HTTP code: ", http_code);
return false;
}
try {
Logger::debug("Authentication response: ", response);
// 解析响应获取 token
nlohmann::json j = nlohmann::json::parse(response);
if (!j.contains("status") || !j.contains("msg") || !j.contains("data")) {
Logger::error("Authentication response missing required fields");
return false;
}
if (j["status"].get<int>() != 200) {
Logger::error("Authentication failed: ", j["msg"].get<std::string>());
return false;
}
auth_token_ = j["data"].get<std::string>();
is_authenticated_ = true;
Logger::debug("Stored token: ", auth_token_);
Logger::info("Successfully authenticated: ", j["msg"].get<std::string>());
return true;
}
catch (const std::exception& e) {
Logger::error("Failed to parse authentication response: ", e.what());
return false;
}
}

View File

@ -13,6 +13,11 @@
class HTTPDataSource : public DataSource {
public:
enum class HttpMethod {
GET,
POST
};
explicit HTTPDataSource(const DataSourceConfig& config);
~HTTPDataSource() override;
@ -40,11 +45,18 @@ private:
static constexpr int HEALTH_CHECK_INTERVAL = 60; // 健康检查间隔(秒)
std::atomic<bool> is_reconnecting_{false}; // 重连状态标志
std::string auth_token_; // 认证 token
bool is_authenticated_; // 认证状态
std::mutex auth_mutex_; // 认证相关的互斥锁
bool authenticate(); // 认证方法
bool refreshToken(); // 刷新 token
bool tryReconnect();
bool ensureConnected();
bool checkConnectionHealth(); // 检查连接健康状况
bool sendRequest(const std::string& path, std::string& response);
bool sendRequest(const std::string& path, std::string& response, HttpMethod method = HttpMethod::GET);
bool parseAircraftResponse(const std::string& response, std::vector<Aircraft>& aircraft);
bool parseVehicleResponse(const std::string& response, std::vector<Vehicle>& vehicles);
bool parseTrafficLightResponse(const std::string& response, std::vector<TrafficLightSignal>& signals);

View File

@ -401,7 +401,7 @@ TEST_F(BasicCollisionTest, QN002AircraftCrossing) {
// 记录详细的测试信息
Logger::debug("QN002-AC001 碰撞测试结果:");
Logger::debug("<EFBFBD><EFBFBD>类型: ", static_cast<int>(result.type));
Logger::debug("类型: ", static_cast<int>(result.type));
Logger::debug("最小距离: ", result.minDistance, "m");
Logger::debug("碰撞时间: ", result.timeToCollision, "s");
Logger::debug("碰撞点: (", result.collisionPoint.x, ",", result.collisionPoint.y, ")");
@ -460,7 +460,7 @@ TEST_F(BasicCollisionTest, CrossingBeforeAndAfter) {
Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")");
Logger::debug("交叉点位置: (", crossPoint.x, ",", crossPoint.y, ")");
ASSERT_FALSE(risks.empty()) << "交叉前应该检测到<EFBFBD><EFBFBD>撞风险";
ASSERT_FALSE(risks.empty()) << "交叉前应该检测到撞风险";
EXPECT_EQ(risks[0].level, RiskLevel::WARNING) << "交叉前应该是预警级别";
// 2. 测试交叉过程中
@ -477,7 +477,7 @@ TEST_F(BasicCollisionTest, CrossingBeforeAndAfter) {
risks = detector_->detectCollisions();
// 记录详细的测试信息
Logger::debug("交叉过程<EFBFBD><EFBFBD>碰撞检测:");
Logger::debug("交叉过程碰撞检测:");
Logger::debug("航空器位置: (", aircraft.position.x, ",", aircraft.position.y, ")");
Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")");

View File

@ -67,6 +67,9 @@ POINT_T11 = {"longitude": 120.0873865, "latitude": 36.3509885}
# QN002 新起点(距离 T6 路口 150 米)
POINT_T12 = {"longitude": 120.08603613, "latitude": 36.35190217} # T4 和 T6 之间,距 T6 150米
# AC001 新起点(距离 T6 路口 200 米)
POINT_T13 = {"longitude": 120.08509148, "latitude": 36.35041247} # T6向南 200 米
# 飞机和车辆尺寸(半径 米)
AIRCRAFT_SIZE_M = 30.0
VEHICLE_SIZE_M = 10.0
@ -551,7 +554,7 @@ def update_aircraft_position(aircraft, elapsed_time):
reached_target = update_position_with_vector(
aircraft,
POINT_T11, # 目标点
POINT_T7, # 起点
POINT_T13, # 起点
aircraft["speed"],
elapsed_time,
return_to_start=True # 航空器需要返回起点
@ -735,8 +738,22 @@ last_aircraft_update_time = time.time()
last_vehicle_update_time = time.time()
last_light_switch_time = time.time()
@app.route('/api/getCurrentFlightPositions')
def check_auth():
auth_header = request.headers.get('Authorization')
if not auth_header or auth_header != AUTH_TOKEN:
return False
return True
@app.route('/api/getCurrentFlightPositions', methods=['GET', 'OPTIONS'])
def get_flight_positions():
if request.method == 'OPTIONS':
return '', 204
if not check_auth():
return jsonify({
"status": 401,
"msg": "认证失败",
"data": None
}), 401
global last_aircraft_update_time
current_time = time.time()
elapsed_time = current_time - last_aircraft_update_time
@ -773,8 +790,16 @@ def switch_traffic_light_state():
if old_state != traffic_light_data[1]["state"]:
print(f"东路口红绿灯状态切换为: {'绿灯' if traffic_light_data[1]['state'] == 1 else '红灯'}")
@app.route('/api/getCurrentVehiclePositions')
@app.route('/api/getCurrentVehiclePositions', methods=['GET', 'OPTIONS'])
def get_vehicle_positions():
if request.method == 'OPTIONS':
return '', 204
if not check_auth():
return jsonify({
"status": 401,
"msg": "认证失败",
"data": None
}), 401
global last_vehicle_update_time
current_time = time.time()
elapsed_time = current_time - last_vehicle_update_time
@ -796,8 +821,16 @@ def get_vehicle_positions():
print(f"Error in get_vehicle_positions: {str(e)}")
return jsonify({"error": str(e)}), 500
@app.route('/api/getTrafficLightSignals')
@app.route('/api/getTrafficLightSignals', methods=['GET', 'OPTIONS'])
def get_traffic_light_signals():
if request.method == 'OPTIONS':
return '', 204
if not check_auth():
return jsonify({
"status": 401,
"msg": "认证失败",
"data": None
}), 401
current_time = time.time()
# 更新时间戳
@ -812,8 +845,9 @@ def get_traffic_light_signals():
@app.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
response.headers['Access-Control-Max-Age'] = '3600'
return response
def check_vehicle_states():
@ -821,5 +855,38 @@ def check_vehicle_states():
for vehicle_no, state in vehicle_states.items():
state.log_state()
# 认证相关配置
AUTH_USERNAME = "dianxin"
AUTH_PASSWORD = "dianxin@123"
AUTH_TOKEN = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY"
@app.route('/api/login', methods=['POST', 'OPTIONS'])
def login():
if request.method == 'OPTIONS':
return '', 204
data = request.get_json()
if not data:
return jsonify({
"status": 400,
"msg": "请求格式错误",
"data": None
}), 400
username = data.get('username')
password = data.get('password')
if username == AUTH_USERNAME and password == AUTH_PASSWORD:
return jsonify({
"status": 200,
"msg": "登入成功",
"data": AUTH_TOKEN
})
else:
return jsonify({
"status": 401,
"msg": "用户名或密码错误",
"data": None
}), 401
if __name__ == '__main__':
app.run(host='localhost', port=8081, debug=True)