Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Build, Push Docker Image, and Deploy

on:
push:
branches: [day3_homework]
pull_request:
branches: [day3_homework]

jobs:
build-and-test-backend:
name: Build And Test Backend Service
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cashe Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Build with Maven
working-directory: ./backend-services
run: mvn -B clean package

build-and-push-images:
name: Build and Push Docker Images
runs-on: ubuntu-latest
needs: build-and-test-backend

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Set up docker buildx
uses: docker/setup-buildx-action@v3

- name: Build and Push API Getway Image
uses: docker/build-push-action@v5
with:
context: ./backend-services
file: ./backend-services/Dockerfile_api_getway
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ai-qa-api-gateway:latest

- name: Build and Push User Service Image
uses: docker/build-push-action@v5
with:
context: ./backend-services
file: ./backend-services/Dockerfile_user_service
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ai-qa-user-service:latest

- name: Build and Push QA Service Image
uses: docker/build-push-action@v5
with:
context: ./backend-services
file: ./backend-services/Dockerfile_qa_service
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ai-qa-qa-service:latest

- name: Build and Push Frontend Image
uses: docker/build-push-action@v5
with:
context: ./frontend-nextjs/frontend
file: ./frontend-nextjs/frontend/Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ai-qa-frontend:latest

- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd ~/airline-cicd-xuwei
docker-compose pull
docker-compose up -d --remove-orphans
docker image prune -f
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/backend-services/api-gateway/target
/backend-services/qa-service/target
/backend-services/user-service/target
27 changes: 27 additions & 0 deletions backend-services/Dockerfile_api_getway
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# === Build stage ===
FROM maven:3.9.9-eclipse-temurin-17-alpine AS build

# WORKDIR /workspace

COPY ./pom.xml ./pom.xml
COPY ./api-gateway/pom.xml ./api-gateway/pom.xml
COPY ./api-gateway/src ./api-gateway/src
COPY ./user-service/pom.xml ./user-service/pom.xml
COPY ./user-service/src ./user-service/src
COPY ./qa-service/pom.xml ./qa-service/pom.xml
COPY ./qa-service/src ./qa-service/src

RUN mvn -pl api-gateway -am -B clean package -DskipTests

# === Runtime stage ===
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

ENV JAVA_OPTS=""

COPY --from=build ./api-gateway/target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
27 changes: 27 additions & 0 deletions backend-services/Dockerfile_qa_service
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# === Build stage ===
FROM maven:3.9.9-eclipse-temurin-17-alpine AS build

# WORKDIR /workspace

COPY ./pom.xml ./pom.xml
COPY ./api-gateway/pom.xml ./api-gateway/pom.xml
COPY ./api-gateway/src ./api-gateway/src
COPY ./user-service/pom.xml ./user-service/pom.xml
COPY ./user-service/src ./user-service/src
COPY ./qa-service/pom.xml ./qa-service/pom.xml
COPY ./qa-service/src ./qa-service/src

RUN mvn -pl qa-service -am -B clean package -DskipTests

# === Runtime stage ===
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

ENV JAVA_OPTS=""

COPY --from=build ./qa-service/target/*.jar app.jar

EXPOSE 8082

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
27 changes: 27 additions & 0 deletions backend-services/Dockerfile_user_service
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# === Build stage ===
FROM maven:3.9.9-eclipse-temurin-17-alpine AS build

# WORKDIR /workspace

COPY ./pom.xml ./pom.xml
COPY ./api-gateway/pom.xml ./api-gateway/pom.xml
COPY ./api-gateway/src ./api-gateway/src
COPY ./user-service/pom.xml ./user-service/pom.xml
COPY ./user-service/src ./user-service/src
COPY ./qa-service/pom.xml ./qa-service/pom.xml
COPY ./qa-service/src ./qa-service/src

RUN mvn -pl user-service -am -B clean package -DskipTests

# === Runtime stage ===
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

ENV JAVA_OPTS=""

COPY --from=build ./user-service/target/*.jar app.jar

EXPOSE 8081

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
5 changes: 5 additions & 0 deletions backend-services/api-gateway/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# .dockerignore
**/target
.idea
.vscode
*.iml
1 change: 1 addition & 0 deletions backend-services/api-gateway/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target/
39 changes: 39 additions & 0 deletions backend-services/api-gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,45 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version> <!-- 使用一个较新的稳定版本 -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<!-- 版本号会从你的 dependencyManagement 中继承 -->
</dependency>
</dependencies>

<!-- 3. 添加构建插件,用于打包成可执行jar -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ai.qa.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ai.qa.gateway.api.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestConfigController {


@Value("${jwt.secret}")
private String jwtSecret;

@GetMapping("/config")
public String login() {
System.out.println("测试config");
return "测试JWT:"+jwtSecret;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.ai.qa.gateway.api.web.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

//@Component
//@RefreshScope // 为了动态刷新JWT密钥
public class AuthenticationFilter implements GlobalFilter, Ordered {

@Value("${jwt.secret}")
private String jwtSecret;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();

// 定义白名单路径,这些路径不需要JWT验证
List<String> whiteList = List.of("/api/user/register", "/api/user/login");
if (whiteList.contains(request.getURI().getPath())) {
return chain.filter(exchange); // 放行
}

String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}

String token = authHeader.substring(7);
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtSecret.getBytes())
.build()
.parseClaimsJws(token)
.getBody();

// 验证通过,可以将用户信息放入请求头,传递给下游服务
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Name", claims.get("username", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}

@Override
public int getOrder() {
// 鉴权过滤器应在日志过滤器之后,在路由之前,优先级要高
return -100;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ai.qa.gateway.infrastructure.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsConfig {

@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(false); // 允许携带凭证
config.addAllowedOrigin("*"); // 允许所有域名(生产环境不建议这么做)
config.addAllowedHeader("*"); // 允许所有Header
config.addAllowedMethod("*"); // 允许所有方法

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter(source);
}
}
Loading