fix(security): 角色资源树回显防权限放大 + 401 刷新防死循环

- assign-resources-form:只保存叶子键 getCheckedKeys(true),回显 setCheckedKeys(leafOnly)
- http.ts:_retry 标记只重放一次 + 排除 login/refresh

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 16:10:32 +08:00
parent 23e228783c
commit 7ae220581a
4 changed files with 22 additions and 14 deletions

View File

@ -65,9 +65,12 @@ export class HttpClient {
return Promise.reject(new ApiError(body.code, body.message))
},
async (error: AxiosError) => {
const config = error.config
// 401 自动刷新并重放一次
if (error.response?.status === 401 && config) {
const config = error.config as (AxiosRequestConfig & { _retry?: boolean }) | undefined
const url = config?.url ?? ''
const isAuthEndpoint = url.includes('/auth/login') || url.includes('/auth/refresh')
// 401 自动刷新并重放一次(登录/刷新接口自身 401 不重放,_retry 防死循环)
if (error.response?.status === 401 && config && !config._retry && !isAuthEndpoint) {
config._retry = true
try {
const token = await this.refresh()
config.headers = config.headers ?? {}

View File

@ -7,13 +7,12 @@
node-key="code"
show-checkbox
:props="{ label: 'name', children: 'children' }"
:default-checked-keys="assigned ?? []"
default-expand-all
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import { useRequest } from '@/composables/useRequest'
import { listResources } from '@/api/resource'
import { listAssignedResources } from '@/api/role'
@ -25,8 +24,8 @@ defineOptions({ name: 'AssignResourcesForm' })
const props = defineProps<{ roleCode: string }>()
interface TreeApi {
getCheckedKeys: () => (string | number)[]
getHalfCheckedKeys: () => (string | number)[]
getCheckedKeys: (leafOnly?: boolean) => (string | number)[]
setCheckedKeys: (keys: (string | number)[], leafOnly?: boolean) => void
}
const treeRef = ref<TreeApi>()
@ -36,10 +35,18 @@ const { data: assigned } = useRequest((signal: AbortSignal) => listAssignedResou
const tree = computed(() => buildTree(resourceList.value ?? []) as TreeNode<Resource>[])
const ready = computed(() => resourceList.value !== undefined && assigned.value !== undefined)
// 回显:仅叶子节点勾选,父节点由叶子自动推导,避免 default-checked-keys 级联全选
watch(ready, async (r) => {
if (r) {
await nextTick()
treeRef.value?.setCheckedKeys(assigned.value ?? [], true)
}
})
function submit(): { resourceCodes: string[] } {
const checked = treeRef.value?.getCheckedKeys() ?? []
const half = treeRef.value?.getHalfCheckedKeys() ?? []
return { resourceCodes: [...new Set([...checked, ...half].map(String))] }
// 只保存叶子节点:半选父节点由叶子推导,避免回显级联造成权限放大
const checked = treeRef.value?.getCheckedKeys(true) ?? []
return { resourceCodes: [...new Set(checked.map(String))] }
}
defineExpose({ submit })

File diff suppressed because one or more lines are too long

View File

@ -68,15 +68,13 @@ export default defineConfig({
},
},
rules: [
...['primary', 'success', 'warning', 'danger', 'info'].flatMap((color) => [
rules: ['primary', 'success', 'warning', 'danger', 'info'].flatMap((color) => [
[`bg-${color}-light-3`, { 'background-color': `var(--wm-color-${color}-light-3)` }],
[`bg-${color}-light-5`, { 'background-color': `var(--wm-color-${color}-light-5)` }],
[`bg-${color}-light-7`, { 'background-color': `var(--wm-color-${color}-light-7)` }],
[`bg-${color}-light-9`, { 'background-color': `var(--wm-color-${color}-light-9)` }],
[`text-${color}-dark-2`, { color: `var(--wm-color-${color}-dark-2)` }],
]),
],
shortcuts: {
// 布局