feat(auth): 重构认证系统,引入 session 会话管理

- 新增 user_sessions 数据库表及 Alembic 迁移
- 实现 SessionService 会话生命周期管理(创建/刷新/撤销/清理)
- 认证流程改用 refresh token cookie + access token 双令牌模式
- 前端实现自动静默刷新、跨标签页同步及设备指纹
- 用户设置页新增会话管理和密码修改功能
- 管理员用户管理新增强制登出和会话查看
- 密码策略增强,支持强度校验和泄露检测
- OAuth 登录流程适配新会话机制
- 新增完整的单元测试和 API 测试覆盖

Closes #232

Co-authored-by: LewisPen <LewisPen@nyadoo.com>
This commit is contained in:
fawney19
2026-03-17 16:34:09 +08:00
parent d480aa11f3
commit c4bb6b8161
58 changed files with 4654 additions and 508 deletions

View File

@@ -6,7 +6,6 @@
<input
ref="inputRef"
:class="inputClass"
:style="inputStyle"
:value="modelValue"
:type="effectiveType"
:autocomplete="autocompleteAttr"
@@ -42,7 +41,6 @@
v-else
ref="inputRef"
:class="inputClass"
:style="inputStyle"
:value="modelValue"
:type="effectiveType"
:autocomplete="autocompleteAttr"
@@ -69,24 +67,6 @@ const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
// 开发环境警告type="password" 已被弃用
const warnPasswordType = import.meta.env.DEV
? (() => {
let warned = false
return () => {
if (!warned) {
warned = true
// eslint-disable-next-line no-console
console.warn(
'[Input] type="password" 已被弃用,请使用 masked 属性代替。\n' +
'示例:<Input v-model="apiKey" masked />\n' +
'masked 属性使用 CSS 遮蔽而非 password 类型,不会触发浏览器密码管理器。'
)
}
}
})()
: () => {}
interface Props {
modelValue?: string | number
class?: string
@@ -99,9 +79,8 @@ interface Props {
size?: 'default' | 'sm'
/**
* 遮蔽显示内容(用于 API Key 等敏感信息)
* 使用 CSS -webkit-text-security 实现,不会触发浏览器密码管理器
* 隐藏态使用真正的 password 输入框,显示态切换为 text
* 同时会显示一个小眼睛按钮用于切换显示/隐藏
* 注意Firefox 不支持 -webkit-text-security会显示明文但仍可通过按钮切换
*/
masked?: boolean
/**
@@ -134,13 +113,10 @@ const shouldDisableAutofill = computed(() => {
return props.disableAutofill ?? false
})
// 始终使用 text 类型,永远不用 password
const effectiveType = computed(() => {
const attrType = attrs.type as string | undefined
// 如果传入 password强制转为 text配合 masked 使用)
if (attrType === 'password') {
warnPasswordType()
return 'text'
const attrType = (attrs.type as string | undefined) ?? 'text'
if (props.masked) {
return isVisible.value ? 'text' : 'password'
}
return attrType
})
@@ -182,16 +158,6 @@ const inputClass = computed(() =>
)
)
// 当 masked 为 true 且未显示时,用 CSS 遮蔽文字
const inputStyle = computed(() => {
if (props.masked && !isVisible.value) {
// 使用 -webkit-text-securityChrome, Safari, Edge 支持)
// Firefox 不支持此属性,会显示明文,但仍可通过小眼睛按钮切换
return { '-webkit-text-security': 'disc' }
}
return undefined
})
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)