导读:本期聚焦于周翰文创作的《如何在 Vue 3 项目中工程化接入 Firezone 自托管 VPN 服务器?》,敬请观看详情。想让 Vue 3 管理后台直接控制自托管 VPN 的账号、设备与访问策略,却不知道该从哪一层封装 API?Firezone 是基于 WireGuard 的开源 VPN 服务器,它提供了 Web 控制台和 REST 接口,适合自行部署。前端工程化的关键在于把认证、请求封装、类型定义和状态管理拆开:用 Axios 实例统一携带 Bearer Token,用 TypeScript 接口描述用户、设备、规则等资源,再通过 Pinia 缓存列表数据并处理加载状态。配置下发时,需要安全地获取 WireGuard 配置文件并触发浏览器下载。生产环境应优先考虑 BFF 代理隐藏令牌,避免把高权限 API Token 暴露在浏览器中。本文从部署 Firezone 开始,逐步演示 Vue 3 中的 API 客户端、组合式函数、Pinia store 以及设备配置下载流程,帮助团队快速搭建一套可维护的 VPN 管理前端。

企业内部服务需要安全访问时,自托管 VPN 能避免把流量交给第三方。Firezone 作为开源方案,底层采用 WireGuard,性能与安全性都比较理想。但官方后台不一定完全贴合团队工作流,此时用 Vue 3 重新构建一个管理界面,可以把用户审批、设备绑定、访问策略整合进内部系统。下面会从部署准备到前端工程化逐步说明。

如何在 Vue 3 项目中工程化接入 Firezone 自托管 VPN 服务器?

部署 Firezone 并准备 API 凭据

Firezone 官方推荐使用 Docker Compose 部署,核心服务包括 Phoenix 应用、PostgreSQL 数据库以及 WireGuard 网关。一个最小化的 compose 配置可以这样写:

version: "3.8"
services:
  firezone:
    image: firezone/firezone:latest
    ports:
      - "51820:51820/udp"
      - "13000:13000/tcp"
    env_file:
      - .env
    depends_on:
      - postgres
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: firezone
      POSTGRES_USER: firezone
      POSTGRES_PASSWORD: change_me

这里的端口 51820 负责 WireGuard 数据面,13000 是 Web 控制台与 API 入口。启动后需要进入容器执行初始化命令,创建管理员账号。之后登录 Web 界面,在安全设置中生成 API Token。建议为前端集成单独创建一个受限角色,只授予用户、设备和规则的读取与创建权限,不要使用管理员 Token,这样即使前端被攻破,影响范围也更小。

如果 Vue 3 应用与服务端不在同一域名下,会触发跨域限制。Firezone 支持配置 CORS 来源,但更稳妥的做法是让前端请求先打到自己的 BFF 层,由 BFF 附带 Token 转发到 Firezone。这种方式能隐藏 Token,也能统一处理权限校验,后文会继续讨论。

用 TypeScript 封装 Firezone API 客户端

前端工程化的第一步是把请求逻辑集中起来,不要在组件里散落 fetch 调用。可以基于 Axios 创建一个实例,统一设置 baseURL、超时和认证头。下面是一个 request 封装:

import axios, { AxiosRequestConfig, AxiosError } from 'axios';

const http = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api',
  timeout: 10000,
});

http.interceptors.request.use((config) => {
  const token = sessionStorage.getItem('firezone_token');
  if (token) {
    config.headers = config.headers ?? {};
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

http.interceptors.response.use(
  (response) => response,
  (error: AxiosError) => {
    if (error.response?.status === 401) {
      window.dispatchEvent(new Event('firezone:unauthorized'));
    }
    return Promise.reject(error);
  }
);

export async function request<T>(config: AxiosRequestConfig): Promise<T> {
  const response = await http.request<T>(config);
  return response.data;
}

上面代码用到了 TypeScript 泛型,让每个接口能返回对应类型。注意 import.meta.env 是 Vite 暴露环境变量的方式,只有以 VITE_ 开头的变量会注入前端。Token 存储选择了 sessionStorage,会话关闭即清除,比长期保存在 localStorage 稍安全一些,但依然有 XSS 风险,生产环境建议配合 BFF 使用 HttpOnly Cookie。

接下来定义 Firezone 资源类型。Firezone 的 REST API 返回 JSON,用户、设备、规则对象大致如下:

export interface FirezoneUser {
  id: string;
  email: string;
  role: 'admin' | 'unprivileged';
  disabled_at: string | null;
}

export interface FirezoneDevice {
  id: string;
  user_id: string;
  name: string;
  public_key: string;
  ipv4: string;
  ipv6: string;
  created_at: string;
}

export interface FirezoneRule {
  id: string;
  action: 'allow' | 'deny';
  destination: string;
  user_id: string | null;
  port: string;
}

有了类型,再封装组合式函数,组件中就不需要关心请求细节。这个函数维护 loading、error 和 data 三个响应式状态:

import { ref } from 'vue';
import { request } from './client';
import type { FirezoneUser } from './types';

export function useUsers() {
  const users = ref<FirezoneUser[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function fetchUsers() {
    loading.value = true;
    error.value = null;
    try {
      users.value = await request<FirezoneUser[]>({ url: '/users' });
    } catch (err: any) {
      error.value = err?.message ?? '加载用户失败';
    } finally {
      loading.value = false;
    }
  }

  return { users, loading, error, fetchUsers };
}

这种 composable 可以复用在用户列表、设备列表等页面。不要把 users.value 直接暴露给模板进行复杂计算,复杂过滤逻辑应该交给随后介绍的 Pinia store。

使用 Pinia 管理用户与设备状态

当多个组件都需要访问用户列表或当前设备状态时,只靠组件内状态会造成重复请求和状态不同步。Pinia 可以把数据缓存在全局 store 中,并对外暴露异步 actions。示例:

import { defineStore } from 'pinia';
import { request } from '../api/client';
import type { FirezoneUser, FirezoneDevice } from '../api/types';

interface FirezoneState {
  users: FirezoneUser[];
  devices: FirezoneDevice[];
  loading: boolean;
}

export const useFirezoneStore = defineStore('firezone', {
  state: (): FirezoneState => ({
    users: [],
    devices: [],
    loading: false,
  }),
  getters: {
    activeUsers(state) {
      return state.users.filter((user) => !user.disabled_at);
    },
    onlineDevices(state) {
      return state.devices.filter((device) => {
        const lastSeen = Date.parse(device.created_at);
        return Date.now() - lastSeen < 24 * 60 * 60 * 1000;
      });
    },
  },
  actions: {
    async loadUsers() {
      this.loading = true;
      try {
        this.users = await request<FirezoneUser[]>({ url: '/users' });
      } finally {
        this.loading = false;
      }
    },
    async createUser(email: string) {
      const user = await request<FirezoneUser>({
        url: '/users',
        method: 'POST',
        data: { email },
      });
      this.users.push(user);
      return user;
    },
  },
});

这里的 onlineDevices getter 只是一个示例,真实场景中需要根据 Firezone 返回的设备状态字段来判断,而不是使用 created_at 做近似。如果后端 API 没有提供在线状态,可以结合 WebSocket 或轮询更新。

在组件中使用 store 时,可以直接调用 await store.loadUsers() 并绑定 store.users。为了避免模板过度依赖复杂判断,建议在 setup 中用 computed 二次整理数据。表单单页中创建用户后,无需手动刷新列表,因为 store 已经把新对象追加到数组。

实现设备配置下载与客户端接入

管理员在 Vue 3 后台创建用户后,下一步通常是为其生成设备并下载 WireGuard 配置文件。Firezone 提供配置下载接口,前端可以调用并转化为文件下载。为了不把 Token 拼接进 URL,最好通过 Axios 以 blob 类型请求,再创建对象 URL:

import { http } from '../client';

export async function downloadDeviceConfig(deviceId: string) {
  const response = await http.get(`/devices/${deviceId}/configuration`, {
    responseType: 'blob',
  });

  const url = window.URL.createObjectURL(response.data);
  const link = document.createElement('a');
  link.href = url;
  link.download = `firezone-${deviceId}.conf`;
  document.body.appendChild(link);
  link.click();
  link.remove();
  window.URL.revokeObjectURL(url);
}

下载得到的 .conf 文件可以直接导入 WireGuard 官方客户端或 Firezone 客户端。移动端用户也可以扫描二维码,如果要在 Vue 3 页面中展示二维码,可以调用后端返回的文本配置,用 qrcode 这类库生成。图片二维码生成属于纯前端操作,不会把配置上传到第三方服务,但要注意页面渲染安全,避免把完整私钥暴露在日志中。

设备配置过程中,前端表单需要校验设备名称、公钥格式等。公钥通常是 Base64 编码的 32 字节,校验时可以检查长度和字符集:

function isValidPublicKey(key: string): boolean {
  return /^[A-Za-z0-9+/]{43}=$/.test(key.trim());
}

服务端仍应做最终校验,前端校验只是为了提升交互体验。对于 VPN 这类安全敏感功能,不要因为前端限制而省略后端验证。

生产环境安全与工程化细节

开发阶段常用 Vite 代理解决跨域。下面配置把 /api 请求转发到本机 Firezone 服务,并保留路径:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:13000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

但在生产环境,更好的架构是通过 BFF 层转发。例如 Node.js 后端或云函数接收前端请求,校验登录态后再附加 Firezone API Token 调用。这样浏览器中根本不会出现 Firezone 的高权限 Token,减少了泄漏面。如果团队规模较小,只能直接从前端调用 Firezone,务必为前端 Token 设置最小权限,并配置 IP 白名单或短期过期策略。

还需要注意错误信息脱敏。Firezone 的 API 错误可能包含内部路径或堆栈信息,不要直接把原始错误展示给用户。可以在 Axios 拦截器里统一提取状态码和通用信息,例如 403 显示无权限,422 展示字段校验错误,其他情况输出通用失败提示。日志记录时也要过滤 Authorization 请求头。

整套前端工程可以拆分为 api、stores、composables 和 views 四层。API 层只关心请求与类型,stores 层处理缓存和业务动作,composables 抽象复用逻辑,views 负责界面交互。这种划分让接入 Firezone 的代码保持在可控范围,后续升级或替换 VPN 后端时,改动也不会蔓延到每个组件。

Vue 3Firezone自托管VPN修改时间:2026-09-19 15:06:32

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0919/59284.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。