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

@@ -247,6 +247,7 @@ import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
import RegisterDialog from './RegisterDialog.vue'
import { authApi } from '@/api/auth'
import { oauthApi, type OAuthProviderInfo } from '@/api/oauth'
import { getClientDeviceId } from '@/utils/deviceId'
import { getApiUrl } from '@/utils/url'
import { getOAuthIcon } from '@/utils/oauth-icons'
@@ -350,7 +351,12 @@ async function handleLogin() {
function handleOAuthLogin(providerType: string) {
// 如果 sessionStorage 中没有 redirectPath用户直接点击登录而非被守卫拦截
// 则不设置,让 AuthCallback 使用默认跳转逻辑
window.location.href = getApiUrl(`/api/oauth/${providerType}/authorize`)
const authorizeUrl = new URL(
getApiUrl(`/api/oauth/${providerType}/authorize`),
window.location.origin,
)
authorizeUrl.searchParams.set('client_device_id', getClientDeviceId())
window.location.href = authorizeUrl.toString()
}
function handleSwitchToRegister() {

View File

@@ -151,15 +151,12 @@
<Input
:id="`pwd-${formNonce}`"
v-model="formData.password"
type="text"
autocomplete="one-time-code"
data-form-type="other"
data-lpignore="true"
data-1p-ignore="true"
masked
autocomplete="new-password"
disable-autofill
:name="`pwd-${formNonce}`"
:placeholder="getPasswordPolicyPlaceholder(props.passwordPolicyLevel)"
required
class="-webkit-text-security-disc"
:disabled="isLoading"
/>
<p
@@ -182,17 +179,20 @@
<Input
:id="`pwd-confirm-${formNonce}`"
v-model="formData.confirmPassword"
type="text"
autocomplete="one-time-code"
data-form-type="other"
data-lpignore="true"
data-1p-ignore="true"
masked
autocomplete="new-password"
disable-autofill
:name="`pwd-confirm-${formNonce}`"
placeholder="再次输入密码"
required
class="-webkit-text-security-disc"
:disabled="isLoading"
/>
<p
v-if="formData.confirmPassword && formData.password !== formData.confirmPassword"
class="text-xs text-destructive"
>
两次输入的密码不一致
</p>
</div>
</form>