Skip to content

独立认证、多语言与权限接入 ​

本节适用于本轮更新后的生成器和配套的 fin-core、fin-locale、fin-ui、fin-micro。旧项目升级依赖后,还需要迁移启动接线;仅更新 npm 包不会自动改写项目源码。先运行 npm run check:runtime 检查安装包是否具备模板使用的导出。

独立登录页与本地菜单 ​

Vue 子应用模板直接提供 src/views/login/index.vue 登录页和 src/views/home/index.vue 主页。独立打开应用时,未登录统一进入 /#/login,提交账号密码经真实后端验证成功后进入 /#/home。登录失败留在登录页,密码在提交时清空。原账号密码登录弹窗已移除。

登录页不挂载业务布局、侧栏和水印;登录后显示独立工作区。已登录访问 /login 会回到主页;退出、清除凭据或认证失效立即移除业务内容并返回登录页。已经登录但缺少功能权限时进入 /forbidden。当前登录流程固定去主页,不读取外部 redirect 参数。

菜单使用本地 src/router/index.ts 声明,通过 createStandaloneMenus 生成,不请求后台菜单。默认入口为主页、业务列表,详情隐藏;permissions 按后端返回的权限过滤菜单并校验直接路由访问。未使用的 router/demo.ts、计数 store 和重复 /demo 入口已移除。后端只需提供身份、权限与认证协议,平台前端无需启动。

嵌入基座时不注册独立登录页、主页,也不安装独立登录守卫;继续使用宿主身份、宿主菜单和页签 SDK。shareToken:false 的独立 API 认证接线仍由业务显式提供,不在宿主页签中弹出登录页。

host 注册表默认 shareToken:false;确实需要共享宿主认证的可信应用必须显式开启。平台模块拆分、CSRF 和生产部署见平台适配与生产配置。

独立开发时设置 Token ​

  1. 使用 fin-micro create 生成 vue 子应用,运行 npm run dev,直接打开子应用地址。
  2. 点击顶栏的「开发设置」,输入后端签发的测试 Token,点击「验证并应用」。输入的是 Token 本身,不包含 Bearer 。
  3. 模板调用 GET /api/session,携带 Authorization: Bearer <token>。后端验证成功后,才建立当前应用的用户与权限。
  4. 点击「清除凭据」立即撤销本地身份;受保护页面、菜单和按钮同时更新。重新设置后可再次访问。

开发设置仅在 Vite DEV 且独立运行 时出现;验证成功后将 Token 保存到当前来源的 localStorage,键为 fin-micro:dev-token:<appId>。刷新或重新打开应用时自动读取,并先向 GET /api/session 验证,再恢复最新身份与权限,不需要重复输入。输入框提交后清空,用户与权限不写入存储。

点击「清除凭据」、退出登录或当前业务请求返回 401 时同步删除本地 Token。恢复时后端返回 401/403 或凭据格式无效也会删除;网络暂不可用或超时则保留候选供下次刷新重试,但不建立登录态。恢复失败仍显示页面和开发设置入口。卸载只清理本轮内存与订阅,保留已验证 Token 以支持刷新。

存储接线位于生成项目 src/bridge/session.ts,只有 import.meta.env.DEV && !isMicroFrontend() && !bridge 时才向 createMicroSession 注入 tokenStorage。宿主嵌入时,包括独立认证模式和缺桥场景,均不读取这份本地调试凭据;生产构建使用正式登录或 Cookie 会话,不启用开发 Token 存储。不同来源由浏览器隔离,同一来源的不同应用使用不同 appId 键。此方案无需将 Token 写进 .env 或 VITE_*,也不会把凭据写入构建产物、URL、日志或公开会话。

验证失败会撤销旧凭据,不保留上一位用户的权限。验证期间只有最后一次有效操作可以提交结果;清除、切换身份、卸载后的晚到响应不会恢复旧用户。Token 格式正确或能解析 JWT 均不代表验证成功。

后端认证适配在哪里 ​

生成项目的 src/apis/auth.ts 是后端接入点。默认协议如下,业务使用 { code, data } 时只在该文件解包:

请求用途响应
GET /api/session手动 Token 验证或 Cookie 恢复{ user: { id: string, name?: string }, permissions: string[], accessToken?: string | null }
POST /api/login正式账号密码登录同上,请求体为 { username, password }
POST /api/logout注销服务端会话任意成功响应

401 表示未登录,403 表示无权限。后端必须返回真实的用户和权限,不提供「默认全部允许」。如果服务端只返回 Token,先调用身份接口取得用户与权限,再让适配器返回结果。

验证、登录和恢复共用身份解码:权限最多 500 项,逐项检查前先检查数量;重复、空值、控制字符和超过 120 字符的权限均拒绝,不截断授权数据。

/login 页用于独立部署的正式登录,生产环境同样提供。独立开发时登录返回的 Token 同样保存到本地;Cookie 登录结果不额外存储 Token。需要 Cookie 会话恢复时设置 VITE_API_CREDENTIALS=true;无开发 Token 时才尝试适配器的 restore(signal),未登录时返回 null。生产 Bearer 凭据默认只存内存,需按项目登录协议提供恢复。Cookie 的跨域、SameSite 和服务端 CORS 策略由项目部署配置。

同源 Cookie 不受 VITE_API_CREDENTIALS=false 阻止。独立认证的 login/logout 和其他业务写接口需在 apis/auth.ts 等适配层接入服务端 CSRF 协议;host 的 platformSecurity.ts 接入点只覆盖 host 平台写接口,不会自动给独立认证请求添加 CSRF。

ts
import type { IndependentAuthAdapter } from '@axewo/fin-micro/vue'

// 在 src/apis/auth.ts 中替换 URL、响应解包与业务登录协议。
export const independentAuth: IndependentAuthAdapter = {
  async verify(token, signal) {
    const response = await fetch('/api/session', {
      signal,
      headers: { Authorization: `Bearer ${token}` },
    })
    if (!response.ok) throw { status: response.status }
    const data = await response.json()
    // 生产项目应验证响应结构;生成模板已提供白名单解码示例。
    return { authenticated: true, user: data.user, permissions: data.permissions }
  },
}

createMicroSession({ auth, tokenStorage? }) 管理验证、登录、恢复、注销与身份代次。session.restore({ cookie: false }) 只验证本地开发 Token;省略参数默认允许无本地 Token 时恢复 Cookie。模板启动时根据 VITE_API_CREDENTIALS 选择 Cookie 恢复。installMicroSession(app, session) 在根组件和路由安装前注入;卸载必须 session.dispose()。每次 createApp 创建一份,不跨应用共享内存会话。

宿主认证模式沿用宿主的 getToken 能力和认证失效票据。VITE_MICRO_AUTH_MODE=auto 默认根据桥能力协商;host 强制要求宿主认证能力;independent 使用子应用自己的认证适配器。嵌入运行的公开平台权限仍来自宿主,子应用独立 API 凭据不能覆盖宿主平台身份。嵌入且选择独立认证时,项目应在业务登录流程中调用该会话的 signIn/restore,开发 Token 面板不会显示。

统一多语言 ​

统一入口名称为 @axewo/fin-locale。公共语言实现维护在该包的 src/locales;业务项目在 src/locales/modules 中按 模块名_语言.ts(也支持 .json)拆分词条,由 src/locales/index.ts 自动收集、按语言和模块命名空间合并,再调用 installFinI18n 并返回当前应用的 i18n 实例。

ts
// src/locales/modules/User_zh-CN.ts:无需重复声明语言或 User 命名空间。
export default { name: '姓名' }
ts
// src/locales/modules/User_en.ts
export default { name: 'Name' }

上述文件组装为 { 'zh-CN': { User: { name: '姓名' } }, en: { User: { name: 'Name' } } },组件通过 t('User.name') 读取,模块名区分大小写。新增文件无需修改入口;模板的 app_zh-CN.ts、app_en.ts 沿用 app.* 词条。

ts
// src/bootstrap.ts:每次挂载创建当前 Vue App 的语言实例。
import setupI18n from './locales'

const i18n = setupI18n(app)

根组件使用 FinLocaleProvider(模板的 AppProviders.vue 已封装),它让 Fin UI 与 Element Plus 共同跟随当前语言和尺寸,项目不再手动导入 Element Plus 语言包或维护另一份语言 ref。

vue
<script setup lang="ts">
import { FinLocaleProvider } from '@axewo/fin-ui'
import { useFinLocale } from '@axewo/fin-locale'
const { t, locale, setLocale } = useFinLocale()
</script>
<template>
  <FinLocaleProvider>
    <button @click="setLocale(locale === 'en' ? 'zh-CN' : 'en')">
      {{ t('order.submit') }}
    </button>
  </FinLocaleProvider>
</template>

语言选择入口读取当前应用 FConfigProvider 的 config.langs,按配置展示名称、顺序和范围;未配置或空列表时默认简体中文与英文。模板的 AppProviders 已透传 config,也可以继承外层 FConfigProvider,内部 FinLocaleProvider 不覆盖语言列表。基座与独立子应用共用同一个 LanguageSelect 组件,不在两处写死选项。

vue
<script setup lang="ts">
import type { FinConfig } from '@axewo/fin-ui'
import AppProviders from './components/AppProviders.vue'
const config: FinConfig = {
  langs: [
    { locale: 'zh-CN', label: '简体中文' },
    { locale: 'en-US', label: 'English' },
    { locale: 'ja-JP', label: '日本語' },
  ],
}
</script>
<template><AppProviders :config="config"><RouterView /></AppProviders></template>

内置语言标识为 zh-CN 和 en;setLocale 使用 normalizeAppLocale 统一处理 en-US、en_US 等别名,并保留 ja-JP 等有效扩展语言。选项不等于翻译词条,日文业务词条需新增 src/locales/modules/order_ja-JP.ts 等文件,由入口自动注册;缺失词条默认回退中文,当前内置 Fin UI 与 Element Plus 语言映射对额外语种也回退中文。fin.* 属于公共词条,业务使用 order.* 等独立命名空间;工厂递归合并业务词条,不覆盖其他公共词条。

宿主调用 host.updatePreferences({ locale }),子应用通过 connectMicroLocale(composer.locale, bridge) 同步。语言变更不递增认证 revision,不使 Token 请求失效;旧宿主没有偏好接口时保留子应用当前语言。桥同步本身不写入浏览器存储。

模板在 bootstrap 中接入 fin-locale 的公共持久化能力:宿主使用 fin-micro:locale:host:<项目名>,独立 Vue 子应用使用 fin-micro:locale:app:<appId>。首次渲染前恢复语言,切换后立即保存,刷新、退出后重新登录或重挂载都保留选择。嵌入子应用不读取或写入独立语言存储,始终优先接受基座偏好;切换独立语言也不会覆盖基座的选择。

ts
import type { Composer } from 'vue-i18n'
import { connectFinLocaleStorage } from '@axewo/fin-locale'

// 宿主或独立应用:在 installFinI18n 后、app.mount 前连接。
const stopLocaleStorage = connectFinLocaleStorage(
  (i18n.global as Composer).locale,
  'my-application:locale',
)
// 卸载或启动失败时调用 stopLocaleStorage()。

存储不可用时语言仍可在当前页面切换;存储值只用于展示偏好,不授予身份或权限。生成器使用配套 fin-locale 版本,并由 check:runtime/doctor 检查持久化导出。已有项目需升级配套包后补上上述 bootstrap 接线;仅升级依赖不会自动修改项目源码。

非组件代码可用 translateFinBuiltin(key, locale) 读取公共词条;完整业务插值使用当前实例的 t。启动边界通过 createStartupLifecycle(load, onFailure, { locale: () => 'en', development: import.meta.env.DEV }) 指定启动语言,不依赖尚未挂载的 Vue。

权限由同一份会话驱动 ​

后端登录/用户信息接口返回的 permissions 是当前用户完整的按钮权限列表。标识全局唯一即可,各子应用直接用对应标识匹配,不要求「应用:资源:操作」格式。以下使用 button_001、button_002、button_003 分别代表查看、新增、删除,接入时替换为后端真实标识。

json
{ "user": { "id": "u1", "name": "张三" }, "permissions": ["button_001", "button_002"] }

已认证用户的列表包含精确字符串 *:*:* 时拥有全部有效权限;*、order:* 和带空格的 *:*:* 没有此含义。普通标识区分大小写、完全匹配。未登录、会话缺失、空白权限名始终拒绝;前端不根据用户名或角色猜测管理员。

按钮权限指令 ​

v-hasPermi 位于 @axewo/fin-micro/vue,底层调用 fin-core/permission。当前生成器的 host/vue、standard/custom 模板均已在 src/bootstrap.ts 注册,页面可以直接使用。已生成的旧项目需要升级配套 fin-core/fin-micro 并补充以下注册;只升级依赖不会自动修改源码。

1. 子应用注册 ​

在已有 src/bootstrap.ts 中,创建当前挂载的 session 和 app 后、app.mount 前调用一次:

ts
import { installMicroSession, installPermissionDirectives } from '@axewo/fin-micro/vue'

// app = createApp(App)、session = createBridgeSession() 沿用当前 bootstrap 创建的实例。
installMicroSession(app, session)
installPermissionDirectives(app, session.identity)
// 继续安装 router、Pinia 等,再执行 app.mount('#app')。

独立运行时 session.identity 来自本应用后端验证;嵌入时由现有 createBridgeSession 接收宿主公开会话。两种模式共用这一处注册,不需要在每个页面注册、再拉取一遍权限或复制一份权限数组。installMicroSession 本身不注册指令,两个安装函数都需要调用。

2. 主应用注册 ​

host 模板使用平台会话控制器,在已有 src/bootstrap.ts 的 controller 创建后注册:

ts
import { installPermissionDirectives } from '@axewo/fin-micro/vue'
import { publicSession } from './platform'

// controller = createPlatformSession() 沿用当前 bootstrap 创建的实例。
installPermissionDirectives(app, () => {
  const session = controller.session.value
  return session ? publicSession(session) : null
})

主应用和每个子应用都注册自己的 Vue App,传入本应用的响应式 ref 或 getter。不要传 session.identity.value 或 controller.session.value 的一次性快照,否则替换身份后不会跟随更新。

3. 页面与表格操作列完整示例 ​

以下页面使用原生按钮,无需额外 UI 注册;换成已安装 Element Plus 的 <el-button> 时写法相同。

vue
<script setup lang="ts">
import { ref } from 'vue'

const rows = ref([{ id: 1001, name: '订单 A' }, { id: 1002, name: '订单 B' }])
const message = ref('')
let nextId = 1003

function add() {
  rows.value.push({ id: nextId++, name: '新订单' })
  message.value = '已新增内存记录'
}
function view(id: number) { message.value = `查看订单 ${id}` }
function remove(id: number) {
  rows.value = rows.value.filter(row => row.id !== id)
  message.value = `已删除内存记录 ${id}`
}
</script>

<template>
  <section>
    <!-- 单个标识:拥有新增权限时显示。 -->
    <button v-hasPermi="'button_002'" @click="add">新增</button>
    <!-- 数组表示任意一个满足即可,不表示同时满足。 -->
    <p v-hasPermi="['button_002', 'button_003']">你可以维护订单</p>
    <table>
      <thead><tr><th>订单</th><th>操作</th></tr></thead>
      <tbody>
        <tr v-for="row in rows" :key="row.id">
          <td>{{ row.name }}</td>
          <td>
            <button v-hasPermi="'button_001'" @click="view(row.id)">查看</button>
            <button v-hasPermi="'button_003'" @click="remove(row.id)">删除</button>
          </td>
        </tr>
      </tbody>
    </table>
    <p role="status">{{ message }}</p>
  </section>
</template>

这是内存操作示例;业务中将处理函数替换为真实 API。已安装 Element Plus 的项目可保留以上 <script setup>,将整个 <template> 替换为下面的操作列示例。指令放在具体 <el-button> 上,不放在 <el-table-column> 上:

vue
<template>
  <section>
    <el-button v-hasPermi="'button_002'" type="primary" @click="add">新增</el-button>
    <el-table :data="rows" row-key="id">
      <el-table-column prop="id" label="编号" />
      <el-table-column prop="name" label="订单" />
      <el-table-column label="操作" width="180">
        <template #default="{ row }">
          <el-button v-hasPermi="'button_001'" link @click="view(row.id)">查看</el-button>
          <el-button v-hasPermi="'button_003'" link type="danger" @click="remove(row.id)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <p role="status">{{ message }}</p>
  </section>
</template>
绑定 / 会话结果
v-hasPermi="'button_001'"拥有该标识或管理员时显示
v-hasPermi="['button_001', 'button_002']"至少拥有其中一个,或管理员时显示
v-hasPermi="requiredPermission"读取 setup 中的变量,变量变化后重新判断
空字符串、空白字符串、[]、未定义或含非法项的数组隐藏,管理员也不放行;不带绑定的 v-hasPermi 也隐藏
未登录、身份加载中、退出或对应权限被撤销隐藏;后续授权恢复显示,无需刷新页面

指令通过 display: none !important 隐藏,授权时恢复元素原有 display,组件仍会创建并执行 setup。适用于原生元素、el-button 等单根组件;多根组件应放在其实际按钮或额外包裹的原生元素上。同一元素不要同时使用 v-show。指令不提供 .all 修饰符;需要全部权限或阻止组件创建时用下面的 v-if。

4. v-if、全部满足与禁用按钮 ​

vue
<script setup lang="ts">
import { usePermission } from '@axewo/fin-micro/vue'

// 使用 installMicroSession 注入的当前会话;也可显式 usePermission(identityRef)。
const { can, hasAny, hasAll } = usePermission()
const canView = can('button_001') // computed,模板自动解包
</script>

<template>
  <button v-if="canView">有查看权限时才创建</button>
  <button v-if="hasAny(['button_002', 'button_003'])">满足任意一个</button>
  <button v-if="hasAll(['button_002', 'button_003'])">同时满足新增和删除</button>
  <button :disabled="!canView">保留展示,权限不足时禁用</button>
</template>

usePermission(identityRef) 保持兼容。can() 返回响应式计算值;其他判断函数应在模板或 computed 内调用,不要把 const allowed = hasPermission('button_001') 的一次性布尔值用作动态状态。按钮指令、Hook、SDK 与路由权限都复用 core 的管理员规则。主应用使用 usePermission 时需显式传入公开 SessionSnapshot 的 ref/getter,因为 host 默认没有注入子应用的 MicroSession。

5. 验证显隐变化 ​

使用真实后端身份分别验证:['button_001'] 只显示查看;['button_001', 'button_002'] 同时显示查看和新增;['*:*:*'] 显示全部;退出后全部隐藏。权限更新需要由现有会话/宿主订阅提交最新 identity,指令随后自动响应;指令不会轮询接口。各子应用对同一个标识使用相同匹配规则,不要求为每个子应用分割权限数组。

前端显隐用于交互控制,服务端仍须对每次业务请求鉴权;隐藏不会阻止通过脚本直接调用处理函数。

路由与菜单权限 ​

ts
const routes = [{
  path: '/orders',
  component: () => import('./views/orders/index.vue'),
  meta: { title: '订单', titleKey: 'order.title', permissions: ['order:read'] },
}]

父子路由逐层检查:子路由不能覆盖父级权限要求。没有 permissions 或为空数组表示不额外要求功能权限;独立模板仍统一要求先登录。列表不要求额外功能权限,详情要求 demo:read;开发者可按业务收紧本地路由和菜单。

独立模板使用 connectPermissionRouter(router, session.identity, '/forbidden', onError, { login: '/login', home: '/home' }),登录页、主页和拒绝页需注册为不同路由。该公共守卫同时处理认证和功能权限,身份变化与懒加载完成时重新检查;不传第五个参数的旧用法保留原有公开路由规则。菜单使用同一 hasRoutePermission 过滤。微前端 MicroRouteView 不执行全局 Router 守卫,因此内置按解析后的路由及注入身份检查;权限撤销时停止渲染受保护内容,错误插槽收到 FORBIDDEN。宿主仍须配置 canOpen,服务端仍须校验每个接口的权限。

utils 文件归属与迁移 ​

能力公共实现生成项目保留内容
HTTP 地址、超时与 Cookie 配置校验fin-core/configappConfig.ts 将公开 Vite 配置映射到普通对象
Axios、动态 Token、可信目录、取消、401 原请求协调fin-core/http/authenticatedvue 专属 requestUtils.ts 创建当前项目客户端并安装会话;host 使用独立 request.ts
脱敏错误分类fin-core/errorserrorUtils.ts 兼容旧导入和项目日志出口
语言工厂、合并、回退、翻译fin-localelocales 内的业务映射和启动注入
身份注入、权限、宿主语言同步、启动重试生命周期fin-micro/vue、fin-micro/startup后端认证适配器与少量启动装配
水印内容、开关、样式fin-micro/watermark 提供组件watermarkConfig.ts 保留业务配置

这些薄入口有明确用途,可保留以兼容已有业务导入。新项目也可直接导入公共包。水印、后台接口协议、环境变量名称和业务菜单属于项目配置,不应作为通用算法放入 fin-core。

认证请求仅向显式可信 API 目录自动注入 Token;绝对 URL、协议相对 URL、目录外地址和被覆盖的 baseURL 不自动获得凭据。凭据获取失败拒绝本次请求,不匿名重发、不自动重试写请求。卸载调用安装器返回的释放函数,旧 401 不能注销后来登录的用户。

迁移顺序:先升级四个配套包并检查导出,再用公共会话替换本地注入,接入真实认证适配器,统一 locales 与 Provider,最后安装独立路由守卫并同步菜单权限。已有宿主平台的登录适配器与授权注册表保留,避免迁移时出现两个并行的宿主身份源。