From 88788dc473b3ab15801432c7ed9c0d411bfeadc0 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 10 Aug 2026 12:24:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/modules/auth/store/auth.ts | 32 ++++++++----------- src/modules/auth/utils/jwt.test.mjs | 8 +++-- src/modules/auth/utils/jwt.ts | 5 ++- .../api/merchantIam.test.mjs | 4 +++ .../tenant-management/api/merchantIam.ts | 4 +++ src/modules/tenant-management/public.ts | 1 + 7 files changed, 30 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 0154080..39edc79 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ pnpm test 一期路由:登录、注册、工作台、我的信息、企业资料、企业成员、角色权限和头程订单。登录、注册、退出、令牌刷新、个人资料、修改密码、企业资料、企业资质、权限资源、角色列表/新增/授权和成员完整维护已经接入一期后端接口;开发代理默认转发到测试环境 `http://8.163.30.128:9000`,可通过 `API_PROXY_TARGET` 覆盖。登录使用后端约定的用户名和 8–72 位密码,注册成功后可立即登录,企业资料审核只影响订单提交资格。 -认证会话保存 Access Token 和轮换后的 Refresh Token,并对并发 401 请求执行单次刷新。刷新或会话上下文加载遇到临时网络故障、超时等非认证错误时保留当前有效 Token,只有服务端明确返回 401/会话失效才清理登录状态。客户端只接受 `app_code` 为 `MERCHANT` 的 JWT;页面权限取 JWT 声明与后端启用权限资源的交集。前端权限仅用于导航和交互控制,服务端仍须按用户、租户和权限编码完成最终授权。 +认证会话保存 Access Token 和轮换后的 Refresh Token,并对并发 401 请求执行单次刷新。刷新或会话上下文加载遇到临时网络故障、超时等非认证错误时保留当前有效 Token,只有服务端明确返回 401/会话失效才清理登录状态。客户端只接受 `app_code` 为 `MERCHANT` 的 JWT;页面权限通过当前用户授权接口实时加载,不从 JWT 携带或解析角色权限。前端权限仅用于导航和交互控制,服务端仍须按用户、客户和权限编码完成最终授权。 登录表单在开发和生产环境均默认留空;URL 显式传入的登录用户名仍优先。源码和构建产物不得包含联调账号、默认密码或开发提示文案。 diff --git a/src/modules/auth/store/auth.ts b/src/modules/auth/store/auth.ts index 5ce4c28..2199641 100644 --- a/src/modules/auth/store/auth.ts +++ b/src/modules/auth/store/auth.ts @@ -6,11 +6,10 @@ import { ApiError } from '@/api/http' import { FRONTEND_TO_MERCHANT_PERMISSION_CODES, TENANT_PERMISSION_GROUPS, - getMerchantPermissionResources, + getCurrentMerchantPermissionCodes, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile, - type MerchantPermissionResource, } from '@/modules/tenant-management/public' import { clearSessionTokens, getAccessToken, getRefreshToken, persistTemporaryAccessToken, persistTokenPair } from '@/api/session' import { decodeMerchantToken, type MerchantJwtPayload } from '@/modules/auth/utils/jwt' @@ -41,15 +40,8 @@ const emptyProfile: ClientAccountProfile = { tenantName: '', } -function effectiveBackendPermissions(payload: MerchantJwtPayload, resources?: MerchantPermissionResource[]) { - const jwtPermissions = new Set(payload.permissions ?? []) - if (!resources) return jwtPermissions - const activeResources = new Set(resources.filter((item) => item.status === 'ACTIVE').map((item) => item.permissionCode)) - return new Set([...jwtPermissions].filter((permission) => activeResources.has(permission))) -} - -function mapPermissions(payload: MerchantJwtPayload, resources?: MerchantPermissionResource[]) { - const backendPermissions = effectiveBackendPermissions(payload, resources) +function mapPermissions(backendPermissionCodes: string[] = []) { + const backendPermissions = new Set(backendPermissionCodes) const result = new Set(['dashboard:view', 'account:profile:view']) for (const permission of backendPermissions) { if (frontendPermissionCodes.has(permission)) result.add(permission) @@ -64,7 +56,7 @@ function currentUser(payload: MerchantJwtPayload, tenantName?: string): CurrentU name: payload.username, account: payload.username, tenantName: tenantName || `企业租户 ${payload.tenant_id}`, - roles: payload.roles?.length ? [...payload.roles] : ['企业成员'], + roles: [], } } @@ -80,14 +72,14 @@ export const useAuthStore = defineStore('client-auth', () => { const permissionCodes = ref([]) const sessionRestored = ref(false) - function applyToken(accessToken: string, resources?: MerchantPermissionResource[]) { + function applyToken(accessToken: string) { const decoded = decodeMerchantToken(accessToken) payload.value = decoded token.value = accessToken user.value = currentUser(decoded, user.value?.tenantName) profile.value.username = decoded.username profile.value.realName = decoded.username - permissionCodes.value = mapPermissions(decoded, resources) + permissionCodes.value = mapPermissions() } function clearSession() { @@ -131,12 +123,14 @@ export const useAuthStore = defineStore('client-auth', () => { async function loadSessionContext() { if (!payload.value) return - const [resources, tenant, personalProfile] = await Promise.all([ - getMerchantPermissionResources(), + const currentPermissionCodes = await getCurrentMerchantPermissionCodes() + const currentAccessToken = getAccessToken() + if (currentAccessToken && currentAccessToken !== token.value) applyToken(currentAccessToken) + permissionCodes.value = mapPermissions(currentPermissionCodes) + const [tenant, personalProfile] = await Promise.all([ getTenantProfile(), getMerchantPersonalProfile(), ]) - permissionCodes.value = mapPermissions(payload.value, resources) user.value = currentUser(payload.value, tenant.tenantName) profile.value = { ...profile.value, @@ -174,7 +168,7 @@ export const useAuthStore = defineStore('client-auth', () => { payload.value = decoded user.value = currentUser(decoded) profile.value = { ...emptyProfile, username: decoded.username, realName: decoded.username } - permissionCodes.value = mapPermissions(decoded) + permissionCodes.value = mapPermissions() sessionRestored.value = false try { await loadSessionContext() @@ -196,7 +190,7 @@ export const useAuthStore = defineStore('client-auth', () => { payload.value = decoded user.value = currentUser(decoded) profile.value = { ...emptyProfile, username: decoded.username, realName: decoded.username } - permissionCodes.value = mapPermissions(decoded) + permissionCodes.value = mapPermissions() sessionRestored.value = false try { await loadSessionContext() diff --git a/src/modules/auth/utils/jwt.test.mjs b/src/modules/auth/utils/jwt.test.mjs index 2449a1f..b0a0bed 100644 --- a/src/modules/auth/utils/jwt.test.mjs +++ b/src/modules/auth/utils/jwt.test.mjs @@ -13,12 +13,10 @@ const payload = { tenant_id: 2001, membership_id: 3001, app_code: 'MERCHANT', - roles: ['MERCHANT_OWNER'], - permissions: ['merchant:profile:view'], exp: 1900000000, } -test('decodes a merchant JWT payload for UI identity and permissions', () => { +test('decodes a merchant JWT payload containing identity without roles or permissions', () => { assert.deepEqual(decodeMerchantToken(tokenFor(payload)), payload) }) @@ -29,3 +27,7 @@ test('rejects tokens from the platform application domain', () => { test('rejects malformed tokens', () => { assert.throws(() => decodeMerchantToken('not-a-jwt'), /登录凭证格式无效/) }) + +test('rejects expired tokens', () => { + assert.throws(() => decodeMerchantToken(tokenFor({ ...payload, exp: 1 })), /登录状态已过期/) +}) diff --git a/src/modules/auth/utils/jwt.ts b/src/modules/auth/utils/jwt.ts index 508caf0..73bdcda 100644 --- a/src/modules/auth/utils/jwt.ts +++ b/src/modules/auth/utils/jwt.ts @@ -6,8 +6,6 @@ export interface MerchantJwtPayload { membership_id: number | string app_code: string client_type?: string - roles?: string[] - permissions?: string[] exp: number } @@ -29,9 +27,10 @@ export function decodeMerchantToken(token: string): MerchantJwtPayload { const payload = value as Partial if (!payload.user_id || !payload.username || !payload.tenant_id || !payload.membership_id || !payload.exp) throw new Error() if (payload.app_code !== 'MERCHANT') throw new Error('账号不属于当前应用端') + if (payload.exp * 1000 <= Date.now()) throw new Error('登录状态已过期') return payload as MerchantJwtPayload } catch (error) { - if (error instanceof Error && error.message === '账号不属于当前应用端') throw error + if (error instanceof Error && ['账号不属于当前应用端', '登录状态已过期'].includes(error.message)) throw error throw new Error('登录凭证无法解析,请重新登录') } } diff --git a/src/modules/tenant-management/api/merchantIam.test.mjs b/src/modules/tenant-management/api/merchantIam.test.mjs index 2ea83d8..ad4bfd3 100644 --- a/src/modules/tenant-management/api/merchantIam.test.mjs +++ b/src/modules/tenant-management/api/merchantIam.test.mjs @@ -20,6 +20,10 @@ test('declares the documented merchant role and member endpoints', () => { assert.match(source, /url: `\/merchant\/v1\/iam\/members\/\$\{membershipId\}\/status`/) assert.match(source, /url: `\/merchant\/v1\/iam\/members\/\$\{membershipId\}\/password`/) assert.match(source, /url: '\/merchant\/v1\/iam\/profile'/) + assert.match(source, /url: '\/merchant\/v1\/iam\/authorization\/permissions'/) + assert.match(authSource, /getCurrentMerchantPermissionCodes\(\)/) + assert.doesNotMatch(authSource, /payload\.permissions/) + assert.doesNotMatch(authSource, /payload\.roles/) }) test('supports tenant profile updates and authenticated qualification downloads', () => { diff --git a/src/modules/tenant-management/api/merchantIam.ts b/src/modules/tenant-management/api/merchantIam.ts index a033a2b..c59d466 100644 --- a/src/modules/tenant-management/api/merchantIam.ts +++ b/src/modules/tenant-management/api/merchantIam.ts @@ -145,6 +145,10 @@ export function getMerchantPermissionResources() { return apiRequest({ method: 'GET', url: '/merchant/v1/iam/permissions' }) } +export function getCurrentMerchantPermissionCodes() { + return apiRequest({ method: 'GET', url: '/merchant/v1/iam/authorization/permissions' }) +} + export function getMerchantPersonalProfile() { return apiRequest({ method: 'GET', url: '/merchant/v1/iam/profile' }) } diff --git a/src/modules/tenant-management/public.ts b/src/modules/tenant-management/public.ts index e33e0e3..ab709b2 100644 --- a/src/modules/tenant-management/public.ts +++ b/src/modules/tenant-management/public.ts @@ -1,5 +1,6 @@ export { getMerchantPermissionResources, + getCurrentMerchantPermissionCodes, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile,