@@ -10,8 +10,9 @@ RUN mkdir -p /var/log/nginx /var/www/certbot
|
|||||||
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY nginx/conf.d/ /etc/nginx/conf.d/
|
COPY nginx/conf.d/ /etc/nginx/conf.d/
|
||||||
|
|
||||||
# 创建自签名证书(仅用于开发,生产环境应挂载真实证书)
|
# 创建 SSL 目录并生成自签名证书(仅用于开发,生产环境应挂载真实证书)
|
||||||
RUN apk add --no-cache openssl && \
|
RUN mkdir -p /etc/nginx/ssl && \
|
||||||
|
apk add --no-cache openssl && \
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
-keyout /etc/nginx/ssl/key.pem \
|
-keyout /etc/nginx/ssl/key.pem \
|
||||||
-out /etc/nginx/ssl/cert.pem \
|
-out /etc/nginx/ssl/cert.pem \
|
||||||
|
|||||||
29
backend/gateway/docker-compose.yml
Normal file
29
backend/gateway/docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: api-gateway
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
# 开发环境:挂载配置便于热更新,生产环境应内嵌在镜像中
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- frontend_asset-helper-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
start_period: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend_asset-helper-network:
|
||||||
|
external: true
|
||||||
@@ -3,25 +3,48 @@ server {
|
|||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
listen [::]:80 default_server;
|
listen [::]:80 default_server;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
return 444;
|
return 444;
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP 重定向到 HTTPS
|
# HTTP 重定向到 HTTPS(生产域名)
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name api.example.com;
|
server_name api.example.com;
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 301 https://$server_name$request_uri;
|
return 301 https://$server_name$request_uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 开发环境 - 直接代理,不重定向到 HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost api-gateway host.docker.internal;
|
||||||
|
|
||||||
|
# 开发环境直接代理,不强制 HTTPS
|
||||||
|
include /etc/nginx/conf.d/services/*.conf;
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 '{"status":"healthy","timestamp":"$time_iso8601"}\n';
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根路径
|
||||||
|
location / {
|
||||||
|
return 200 '{"status":"ok","service":"api-gateway","timestamp":"$time_iso8601"}\n';
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# API 网关主配置
|
# API 网关主配置
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
|
|||||||
@@ -1,36 +1,62 @@
|
|||||||
# 用户服务路由
|
# 用户服务路由
|
||||||
location /api/v1/users {
|
|
||||||
# 限流
|
# 账号登录(严格限流)
|
||||||
|
location /api/v1/auth/login/account {
|
||||||
|
limit_req zone=api_strict burst=5 nodelay;
|
||||||
|
limit_conn addr 3;
|
||||||
|
|
||||||
|
rewrite ^/api/v1/auth/login/account$ /login break;
|
||||||
|
proxy_pass http://user_login_account;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Request-ID $request_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 邮箱登录(严格限流)
|
||||||
|
location /api/v1/auth/login/email {
|
||||||
|
limit_req zone=api_strict burst=5 nodelay;
|
||||||
|
limit_conn addr 3;
|
||||||
|
|
||||||
|
rewrite ^/api/v1/auth/login/email$ /login break;
|
||||||
|
proxy_pass http://user_login_email;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Request-ID $request_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 账号注册(通用限流)
|
||||||
|
location /api/v1/users/register/account {
|
||||||
limit_req zone=general burst=20 nodelay;
|
limit_req zone=general burst=20 nodelay;
|
||||||
limit_conn addr 10;
|
limit_conn addr 10;
|
||||||
|
|
||||||
# 代理设置
|
rewrite ^/api/v1/users/register/account$ /register break;
|
||||||
proxy_pass http://user_service;
|
proxy_pass http://user_register_account;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Request-ID $request_id;
|
proxy_set_header X-Request-ID $request_id;
|
||||||
|
|
||||||
# WebSocket 支持(如果需要)
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
# 缓存控制
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_no_cache 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 认证相关接口(严格限流)
|
# 邮箱注册(通用限流)
|
||||||
location /api/v1/auth {
|
location /api/v1/users/register/email {
|
||||||
limit_req zone=api_strict burst=5 nodelay;
|
limit_req zone=general burst=20 nodelay;
|
||||||
limit_conn addr 3;
|
limit_conn addr 10;
|
||||||
|
|
||||||
proxy_pass http://user_service;
|
rewrite ^/api/v1/users/register/email$ /register break;
|
||||||
|
proxy_pass http://user_register_email;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
@@ -44,22 +44,42 @@ http {
|
|||||||
# 连接限制
|
# 连接限制
|
||||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||||
|
|
||||||
# 上游服务
|
# 上游服务 —— 通过宿主机端口访问各微服务(开发环境)
|
||||||
upstream user_service {
|
# 生产环境应改为容器名:端口,并确保同网络
|
||||||
|
upstream user_login_account {
|
||||||
least_conn;
|
least_conn;
|
||||||
server user-service:8080 max_fails=3 fail_timeout=30s;
|
server host.docker.internal:20111 max_fails=3 fail_timeout=30s;
|
||||||
keepalive 32;
|
keepalive 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream user_register_account {
|
||||||
|
least_conn;
|
||||||
|
server host.docker.internal:20112 max_fails=3 fail_timeout=30s;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream user_login_email {
|
||||||
|
least_conn;
|
||||||
|
server host.docker.internal:20113 max_fails=3 fail_timeout=30s;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream user_register_email {
|
||||||
|
least_conn;
|
||||||
|
server host.docker.internal:20114 max_fails=3 fail_timeout=30s;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 以下服务尚未实现,临时标记为 down,避免启动时 DNS 解析失败
|
||||||
upstream order_service {
|
upstream order_service {
|
||||||
least_conn;
|
least_conn;
|
||||||
server order-service:8080 max_fails=3 fail_timeout=30s;
|
server 127.0.0.1:9999 down;
|
||||||
keepalive 32;
|
keepalive 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream payment_service {
|
upstream payment_service {
|
||||||
least_conn;
|
least_conn;
|
||||||
server payment-service:8080 max_fails=3 fail_timeout=30s;
|
server 127.0.0.1:9999 down;
|
||||||
keepalive 32;
|
keepalive 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,18 +28,12 @@ struct LoginRequest {
|
|||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一响应包装
|
// 登录/认证类接口扁平响应(与前端约定对齐)
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct ApiResponse<T> {
|
struct LoginResponse {
|
||||||
success: bool,
|
success: bool,
|
||||||
|
token: Option<String>,
|
||||||
message: String,
|
message: String,
|
||||||
data: Option<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录业务数据
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct LoginData {
|
|
||||||
token: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT Claims
|
// JWT Claims
|
||||||
@@ -98,7 +92,7 @@ async fn main() {
|
|||||||
async fn login_handler(
|
async fn login_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(payload): Json<LoginRequest>,
|
Json(payload): Json<LoginRequest>,
|
||||||
) -> (StatusCode, Json<ApiResponse<LoginData>>) {
|
) -> (StatusCode, Json<LoginResponse>) {
|
||||||
info!("Login attempt for user: {}", payload.username);
|
info!("Login attempt for user: {}", payload.username);
|
||||||
|
|
||||||
// 查询用户账号与密码
|
// 查询用户账号与密码
|
||||||
@@ -120,16 +114,16 @@ async fn login_handler(
|
|||||||
match verify(&payload.password, &password_hash) {
|
match verify(&payload.password, &password_hash) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
info!("User {} logged in successfully", payload.username);
|
info!("User {} logged in successfully", payload.username);
|
||||||
|
|
||||||
// 生成 JWT
|
// 生成 JWT
|
||||||
let token = generate_token(&user_id.to_string(), &state.jwt_secret);
|
let token = generate_token(&user_id.to_string(), &state.jwt_secret);
|
||||||
|
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(ApiResponse {
|
Json(LoginResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
token: Some(token),
|
||||||
message: "Login successful".to_string(),
|
message: "Login successful".to_string(),
|
||||||
data: Some(LoginData { token }),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,10 +131,10 @@ async fn login_handler(
|
|||||||
warn!("Invalid password for user {}", payload.username);
|
warn!("Invalid password for user {}", payload.username);
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(ApiResponse {
|
Json(LoginResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
token: None,
|
||||||
message: "Invalid credentials".to_string(),
|
message: "Invalid credentials".to_string(),
|
||||||
data: None,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,10 +142,10 @@ async fn login_handler(
|
|||||||
warn!("Password verification error: {:?}", e);
|
warn!("Password verification error: {:?}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(ApiResponse {
|
Json(LoginResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
token: None,
|
||||||
message: "Internal error".to_string(),
|
message: "Internal error".to_string(),
|
||||||
data: None,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,18 +28,12 @@ struct LoginRequest {
|
|||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一响应包装
|
// 登录/认证类接口扁平响应(与前端约定对齐)
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct ApiResponse<T> {
|
struct LoginResponse {
|
||||||
success: bool,
|
success: bool,
|
||||||
|
token: Option<String>,
|
||||||
message: String,
|
message: String,
|
||||||
data: Option<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录业务数据
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct LoginData {
|
|
||||||
token: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT Claims
|
// JWT Claims
|
||||||
@@ -98,7 +92,7 @@ async fn main() {
|
|||||||
async fn login_handler(
|
async fn login_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(payload): Json<LoginRequest>,
|
Json(payload): Json<LoginRequest>,
|
||||||
) -> (StatusCode, Json<ApiResponse<LoginData>>) {
|
) -> (StatusCode, Json<LoginResponse>) {
|
||||||
info!("Login attempt for email: {}", payload.email);
|
info!("Login attempt for email: {}", payload.email);
|
||||||
|
|
||||||
// 查询用户邮箱与密码
|
// 查询用户邮箱与密码
|
||||||
@@ -120,16 +114,16 @@ async fn login_handler(
|
|||||||
match verify(&payload.password, &password_hash) {
|
match verify(&payload.password, &password_hash) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
info!("Email {} logged in successfully", payload.email);
|
info!("Email {} logged in successfully", payload.email);
|
||||||
|
|
||||||
// 生成 JWT
|
// 生成 JWT
|
||||||
let token = generate_token(&user_id.to_string(), &state.jwt_secret);
|
let token = generate_token(&user_id.to_string(), &state.jwt_secret);
|
||||||
|
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(ApiResponse {
|
Json(LoginResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
token: Some(token),
|
||||||
message: "Login successful".to_string(),
|
message: "Login successful".to_string(),
|
||||||
data: Some(LoginData { token }),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,10 +131,10 @@ async fn login_handler(
|
|||||||
warn!("Invalid password for email {}", payload.email);
|
warn!("Invalid password for email {}", payload.email);
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(ApiResponse {
|
Json(LoginResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
token: None,
|
||||||
message: "Invalid credentials".to_string(),
|
message: "Invalid credentials".to_string(),
|
||||||
data: None,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,10 +142,10 @@ async fn login_handler(
|
|||||||
warn!("Password verification error: {:?}", e);
|
warn!("Password verification error: {:?}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(ApiResponse {
|
Json(LoginResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
token: None,
|
||||||
message: "Internal error".to_string(),
|
message: "Internal error".to_string(),
|
||||||
data: None,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export default defineConfig(({ mode }) => ({
|
|||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: process.env.VITE_API_BASE_URL || 'http://host.docker.internal:80',
|
// 开发环境:通过 Docker 网络直接访问网关容器
|
||||||
|
target: process.env.VITE_API_BASE_URL || 'http://api-gateway',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user