This commit is contained in:
QuarkTree 2025-03-18 07:43:46 +08:00
commit d2e93a2736
Signed by: QuarkTree
GPG Key ID: 2D142EF046222686
26 changed files with 918 additions and 0 deletions

232
README.md Normal file
View File

@ -0,0 +1,232 @@
以下是 `README.md` 的完整内容,包含项目的概述、功能、安装步骤、配置说明和使用方法。
---
# Firewall Management System
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Go Version](https://img.shields.io/badge/go-1.20%2B-brightgreen)
![Vue Version](https://img.shields.io/badge/vue-3.2%2B-brightgreen)
Firewall Management System 是一个基于 Go 和 Vue3 的防火墙管理系统支持多用户、IP 白名单管理、黑名单管理以及管理员审批流程。通过自动获取用户 IP 地址,简化了 IP 白名单的申请和审批流程。
---
## 功能特性
- **用户管理**
- 普通用户注册与登录
- 管理员账户管理
- 多级权限控制(普通用户、管理员、超级管理员)
- **IP 白名单管理**
- 自动获取用户 IP 地址
- 用户申请 IP 白名单
- 管理员审批 IP 请求
- 自动更新 iptables 规则
- **黑名单管理**
- 支持按用户或 IP 封禁
- 自动撤销被封禁用户的 IP 权限
- 实时同步 iptables 规则
- **安全特性**
- Cloudflare Turnstile 验证码
- 请求频率限制
- 审计日志记录
- 会话管理加固
- **部署支持**
- 支持 Docker 容器化部署
- Systemd 服务管理
- Nginx 反向代理配置
---
## 技术栈
- **后端**
- Go 1.20+
- Gorilla Mux 路由
- SQLite 数据库
- iptables 规则管理
- **前端**
- Vue 3
- Pinia 状态管理
- Vite 构建工具
- Axios HTTP 客户端
- **部署**
- Docker
- Systemd
- Nginx
---
## 安装与部署
### 1. 环境要求
- Linux 服务器(推荐 Ubuntu 22.04 LTS
- Go 1.20+
- Node.js 16+
- SQLite3
- iptables
### 2. 克隆项目
```bash
git clone hhttps://code.kcpot.top/QuarkTree/firewall-system.git
cd firewall-system
```
### 3. 配置环境变量
`backend/config/.env` 中配置以下变量:
```env
SESSION_SECRET="your-32byte-secret-key"
CF_SITE_KEY="your-cloudflare-sitekey"
CF_SECRET_KEY="your-cloudflare-secret"
DEFAULT_ADMIN="superadmin"
DEFAULT_ADMIN_PASS="SecurePass123!"
```
### 4. 初始化数据库
```bash
sqlite3 backend/db/firewall.db < backend/scripts/init_db.sql
```
### 5. 初始化管理员账户
```bash
chmod +x backend/scripts/init_admin.sh
./backend/scripts/init_admin.sh
```
### 6. 构建与运行
#### 后端
```bash
cd backend
go mod tidy
go build -o firewall
sudo ./firewall
```
#### 前端
```bash
cd frontend
npm install
npm run build
npx serve -s dist -p 3000
```
### 7. 使用 Docker 部署
```bash
cd deploy/docker
docker-compose up -d
```
---
## 配置文件
### Nginx 配置 (`deploy/nginx/firewall.conf`)
```nginx
server {
listen 80;
server_name firewall.example.com;
location /api {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /var/www/firewall-frontend;
try_files $uri $uri/ /index.html;
}
}
```
### Systemd 服务文件 (`deploy/systemd/firewall.service`)
```ini
[Unit]
Description=Firewall Management Service
After=network.target
[Service]
EnvironmentFile=/opt/firewall/config/.env
WorkingDirectory=/opt/firewall/backend
ExecStart=/usr/local/bin/firewall
Restart=always
User=firewall-user
[Install]
WantedBy=multi-user.target
```
---
## 使用指南
### 1. 用户注册与登录
- 访问 `/register` 注册新用户
- 访问 `/login` 登录系统
### 2. 申请 IP 白名单
- 登录后访问 `/user/dashboard`
- 点击“申请新 IP”按钮
### 3. 管理员审批
- 管理员登录后访问 `/admin/dashboard`
- 查看待审批请求并批准或拒绝
### 4. 黑名单管理
- 管理员访问 `/admin/blacklist`
- 添加或移除黑名单条目
---
## API 文档
### 公共接口
- `POST /api/register` - 用户注册
- `POST /api/login` - 用户登录
### 用户接口
- `POST /api/user/request` - 申请 IP 白名单
- `GET /api/user/requests` - 获取用户请求状态
### 管理接口
- `GET /api/admin/requests` - 获取待审批请求
- `POST /api/admin/approve` - 批准或拒绝请求
- `POST /api/admin/blacklist` - 管理黑名单
---
## 贡献指南
欢迎提交 Issue 和 Pull Request
请确保代码风格一致,并通过所有测试。
---
## 许可证
本项目采用 [MIT 许可证](LICENSE)。

0
backend/config/.env Normal file
View File

11
backend/go.mod Normal file
View File

@ -0,0 +1,11 @@
module firewall-system
go 1.20
require (
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
golang.org/x/crypto v0.9.0
golang.org/x/time v0.3.0
modernc.org/sqlite v1.21.1
)

10
backend/go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/gorilla/mux v1.8.0 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/sessions v1.2.1 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbM=
github.com/gorilla/sessions v1.2.1/go.mod h1:8KCfur6+4TmpsXA+g09ya2YtYYsFw5D7sE4fLH1NfK4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
modernc.org/sqlite v1.21.1 h1:6JQQqG9n7z7ZtL0vZ+5nZ5y5y5Z5y5Z5y5Z5y5Z5y5Z5=
modernc.org/sqlite v1.21.1/go.mod h1:5ZQP2yCQ6z+X5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5=

54
backend/main.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
var (
db *sql.DB
store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
)
func main() {
initDB()
defer db.Close()
r := mux.NewRouter()
r.Use(middleware.CORS, middleware.RateLimit, middleware.AuditLog)
// 公共路由
r.HandleFunc("/api/register", registerHandler).Methods("POST")
r.HandleFunc("/api/login", loginHandler).Methods("POST")
// 用户路由
userRouter := r.PathPrefix("/api/user").Subrouter()
userRouter.Use(middleware.Auth)
userRouter.HandleFunc("/request", submitIPRequestHandler).Methods("POST")
// 管理路由
adminRouter := r.PathPrefix("/api/admin").Subrouter()
adminRouter.Use(middleware.AdminAuth)
adminRouter.HandleFunc("/approve", approveRequestHandler).Methods("POST")
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
func initDB() {
var err error
db, err = sql.Open("sqlite", "db/firewall.db")
if err != nil {
log.Fatal("Failed to open database:", err)
}
}

View File

@ -0,0 +1,21 @@
package middleware
import (
"log"
"net/http"
"time"
)
// 审计日志中间件
func AuditLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf(
"Method=%s Path=%s Duration=%s",
r.Method,
r.URL.Path,
time.Since(start),
)
})
}

View File

@ -0,0 +1,32 @@
package middleware
import (
"net/http"
"github.com/gorilla/sessions"
)
var store = sessions.NewCookieStore([]byte("your-secret-key"))
// 用户认证中间件
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session")
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "未授权访问", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// 管理员认证中间件
func AdminAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session")
if role, ok := session.Values["role"].(string); !ok || role != "admin" {
http.Error(w, "需要管理员权限", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,19 @@
package middleware
import (
"net/http"
"golang.org/x/time/rate"
)
var limiter = rate.NewLimiter(rate.Every(1*time.Minute), 5)
// 请求频率限制中间件
func RateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "请求过于频繁", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,17 @@
#!/bin/bash
DB_PATH="db/firewall.db"
ADMIN_USER="superadmin"
ADMIN_PASS="ChangeThisPassword123!"
# 生成密码哈希
HASH=$(echo -n "$ADMIN_PASS" | bcrypt-cli -c 12)
sqlite3 "$DB_PATH" <<EOF
INSERT INTO users (username, password_hash, role)
VALUES ('$ADMIN_USER', '$HASH', 'superadmin');
EOF
echo "管理员账户初始化完成"
echo "用户名: $ADMIN_USER"
echo "密码: $ADMIN_PASS"

View File

@ -0,0 +1,28 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS ip_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
ip TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT,
ip TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
COMMIT;

18
deploy/docker/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o firewall
FROM debian:bullseye
RUN apt-get update && apt-get install -y sqlite3
COPY --from=builder /app/firewall /usr/local/bin/
COPY scripts/init_admin.sh .
RUN sqlite3 /var/lib/firewall.db < scripts/init_db.sql && \
chmod +x init_admin.sh && \
./init_admin.sh && \
rm init_admin.sh
CMD ["firewall"]

View File

@ -0,0 +1,14 @@
server {
listen 80;
server_name firewall.example.com;
location /api {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /var/www/firewall-frontend;
try_files $uri $uri/ /index.html;
}
}

View File

@ -0,0 +1,12 @@
[Unit]
Description=Firewall Frontend Service
After=network.target
[Service]
WorkingDirectory=/opt/firewall/frontend
ExecStart=/usr/bin/npm run preview -- --port 3000 --host 0.0.0.0
Restart=always
User=www-data
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,13 @@
[Unit]
Description=Firewall Management Service
After=network.target
[Service]
EnvironmentFile=/opt/firewall/config/.env
WorkingDirectory=/opt/firewall/backend
ExecStart=/usr/local/bin/firewall
Restart=always
User=firewall-user
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,29 @@
<template>
<div class="cf-turnstile">
<div ref="turnstileWidget"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const turnstileWidget = ref(null)
const emit = defineEmits(['verify'])
onMounted(() => {
if (window.turnstile) {
window.turnstile.render(turnstileWidget.value, {
sitekey: 'your-site-key',
callback: (token) => {
emit('verify', token)
},
})
}
})
</script>
<style scoped>
.cf-turnstile {
margin: 1rem 0;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<nav class="navbar">
<router-link to="/">首页</router-link>
<router-link to="/user/dashboard">用户面板</router-link>
<router-link to="/admin/dashboard" v-if="isAdmin">管理面板</router-link>
<button @click="logout">退出</button>
</nav>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const router = useRouter()
const isAdmin = computed(() => authStore.user?.role === 'admin')
const logout = () => {
authStore.logout()
router.push('/login')
}
</script>
<style scoped>
.navbar {
display: flex;
gap: 1rem;
padding: 1rem;
background: #f0f0f0;
}
</style>

9
frontend/src/main.js Normal file
View File

@ -0,0 +1,9 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

18
frontend/src/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "firewall-frontend",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.3.4",
"pinia": "^2.0.33",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
},
"devDependencies": {
"vite": "^4.1.4"
}
}

View File

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/user/dashboard'
},
{
path: '/user/dashboard',
component: () => import('../views/User/Dashboard.vue')
},
{
path: '/admin/dashboard',
component: () => import('../views/Admin/Dashboard.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const router = useRouter()
// 登录
const login = async (username, password) => {
const response = await axios.post('/api/login', {
username,
password
})
user.value = response.data.user
localStorage.setItem('token', response.data.token)
router.push('/user/dashboard')
}
// 登出
const logout = () => {
user.value = null
localStorage.removeItem('token')
router.push('/login')
}
// 检查登录状态
const checkAuth = async () => {
try {
const response = await axios.get('/api/user/me')
user.value = response.data
} catch (error) {
logout()
}
}
return {
user,
login,
logout,
checkAuth
}
})

View File

@ -0,0 +1,27 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'
export const useRequestsStore = defineStore('requests', () => {
const pendingRequests = ref([])
const approvedRequests = ref([])
// 获取待审批请求
const fetchPendingRequests = async () => {
const response = await axios.get('/api/admin/requests')
pendingRequests.value = response.data
}
// 获取已批准请求
const fetchApprovedRequests = async () => {
const response = await axios.get('/api/user/requests')
approvedRequests.value = response.data
}
return {
pendingRequests,
approvedRequests,
fetchPendingRequests,
fetchApprovedRequests
}
})

View File

@ -0,0 +1,43 @@
<template>
<div class="blacklist">
<h2>黑名单管理</h2>
<div v-if="blacklist.length > 0">
<ul>
<li v-for="item in blacklist" :key="item.id">
{{ item.target }} - {{ item.type }}
<button @click="removeFromBlacklist(item.id)">移除</button>
</li>
</ul>
</div>
<div>
<input v-model="newTarget" placeholder="输入IP或用户名">
<button @click="addToBlacklist">加入黑名单</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const blacklist = ref([])
const newTarget = ref('')
const fetchBlacklist = async () => {
const response = await axios.get('/api/admin/blacklist')
blacklist.value = response.data
}
const addToBlacklist = async () => {
await axios.post('/api/admin/blacklist', { target: newTarget.value })
newTarget.value = ''
await fetchBlacklist()
}
const removeFromBlacklist = async (id) => {
await axios.delete(`/api/admin/blacklist/${id}`)
await fetchBlacklist()
}
onMounted(fetchBlacklist)
</script>

View File

@ -0,0 +1,139 @@
<template>
<div class="admin-dashboard">
<h2>管理员面板</h2>
<div v-if="pendingRequests.length > 0">
<h3>待审批请求</h3>
<table class="requests-table">
<thead>
<tr>
<th>用户</th>
<th>IP 地址</th>
<th>申请时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="req in pendingRequests" :key="req.id">
<td>{{ req.username }}</td>
<td>{{ req.ip }}</td>
<td>{{ formatDate(req.created_at) }}</td>
<td>
<button @click="approveRequest(req.id)" class="approve-btn">批准</button>
<button @click="rejectRequest(req.id)" class="reject-btn">拒绝</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<p>没有待审批的请求</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
//
const pendingRequests = ref([])
//
const fetchPendingRequests = async () => {
try {
const response = await axios.get('/api/admin/requests')
pendingRequests.value = response.data
} catch (error) {
console.error('获取待审批请求失败:', error)
}
}
//
const approveRequest = async (requestId) => {
try {
await axios.post('/api/admin/approve', { request_id: requestId })
await fetchPendingRequests()
alert('请求已批准')
} catch (error) {
console.error('批准请求失败:', error)
alert('操作失败,请重试')
}
}
//
const rejectRequest = async (requestId) => {
try {
await axios.post('/api/admin/reject', { request_id: requestId })
await fetchPendingRequests()
alert('请求已拒绝')
} catch (error) {
console.error('拒绝请求失败:', error)
alert('操作失败,请重试')
}
}
//
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString()
}
//
onMounted(fetchPendingRequests)
</script>
<style scoped>
.admin-dashboard {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.requests-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.requests-table th,
.requests-table td {
padding: 12px;
border: 1px solid #ddd;
text-align: left;
}
.requests-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.requests-table tr:hover {
background-color: #f9f9f9;
}
.approve-btn {
background-color: #4caf50;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-right: 8px;
}
.approve-btn:hover {
background-color: #45a049;
}
.reject-btn {
background-color: #f44336;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
}
.reject-btn:hover {
background-color: #e53935;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="dashboard">
<h2>用户面板</h2>
<div v-if="requests.length > 0">
<h3>我的请求</h3>
<ul>
<li v-for="req in requests" :key="req.id">
{{ req.ip }} - {{ req.status }}
</li>
</ul>
</div>
<button @click="requestNewIP">申请新IP</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRequestsStore } from '../stores/requests'
const requestsStore = useRequestsStore()
const requests = ref([])
const requestNewIP = async () => {
await axios.post('/api/user/request')
await requestsStore.fetchApprovedRequests()
}
onMounted(async () => {
await requestsStore.fetchApprovedRequests()
requests.value = requestsStore.approvedRequests
})
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="request-form">
<h3>申请新IP</h3>
<form @submit.prevent="submitRequest">
<CfTurnstile @verify="setToken" />
<button type="submit" :disabled="!token">提交申请</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import CfTurnstile from '../components/CfTurnstile.vue'
const token = ref('')
const setToken = (newToken) => {
token.value = newToken
}
const submitRequest = async () => {
await axios.post('/api/user/request', { token: token.value })
alert('申请已提交')
}
</script>

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})