127 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Request, Response, NextFunction, RequestHandler } from 'express'
import { keycloak } from '../utils/keycloak'
import {
AuthIdentityError,
resolveAuthUserById,
resolveOrProvisionAuthUser
} from '../utils/authIdentity'
import { PERMISSIONS, type Permission } from '../utils/permissions'
import { findEligibleDingTalkSessionUser } from '../services/dingtalkBrokerLoginService'
import { resolveAuthMode } from '../utils/authMode'
const isLocalAuth = () => resolveAuthMode() === 'local'
const hasRealBearerToken = (req: Request) => {
const authorization = req.headers.authorization
const match = authorization?.match(/^Bearer\s+(.+)$/i)
const token = match?.[1]?.trim()
return Boolean(token && token.toLowerCase() !== 'session')
}
// 选择保护策略:local 使用会话校验;默认使用 Keycloak 保护
export function protect(): RequestHandler {
const keycloakMiddleware = isLocalAuth() ? null : keycloak.protect()
return async function authWrapper(req: Request, res: Response, next: NextFunction) {
const authenticateSession = async () => {
const sessionUser = (req as any).session?.user
const sessionUserId = Number(sessionUser?.id)
const authSource = sessionUser?.auth_source === 'dingtalk' ? 'dingtalk' : undefined
const sessionDingTalkUserId = authSource === 'dingtalk'
? String(sessionUser?.dingtalk_userid || '').trim()
: ''
if (!Number.isInteger(sessionUserId) || sessionUserId <= 0) {
return false
}
try {
if (authSource === 'dingtalk') {
const eligibleUser = await findEligibleDingTalkSessionUser(sessionUserId, sessionDingTalkUserId)
if (!eligibleUser) {
throw new AuthIdentityError('钉钉绑定已失效,请重新登录或联系管理员', 401)
}
}
const currentUser = await resolveAuthUserById(sessionUserId)
;(req as any).session.user = authSource
? {
id: currentUser.id,
auth_source: authSource,
dingtalk_userid: sessionDingTalkUserId
}
: { id: currentUser.id }
res.locals.user = currentUser
next()
return true
} catch (error: any) {
if ((req as any).session) {
;(req as any).session.user = undefined
}
const status = error instanceof AuthIdentityError ? error.status : 401
res.status(status).json({ code: status, message: error.message || '认证失败', data: null })
return true
}
}
const authenticateKeycloak = () => keycloakMiddleware?.(req, res, async (err?: any) => {
if (err) {
return next(err)
}
const tokenContent = (req as any).kauth?.grant?.access_token?.content as Record<string, any> | undefined
if (tokenContent) {
try {
const resolvedUser = await resolveOrProvisionAuthUser({
username: tokenContent.preferred_username,
name: tokenContent.name || tokenContent.preferred_username,
email: tokenContent.email,
roles: tokenContent.realm_access?.roles || []
})
res.locals.user = resolvedUser
} catch (resolveError) {
return next(resolveError)
}
}
return next()
})
// 本地模式只认服务端会话,不接受 Bearer 令牌改变当前身份。
if (!keycloakMiddleware) {
if (await authenticateSession()) return
return res.status(401).json({ code: 401, message: '未登录', data: null })
}
// Keycloak 模式下,真实 Bearer 令牌优先于浏览器里可能残留的钉钉 H5 会话。
// 前端本地登录使用的 `Bearer session` 只是占位值,仍按会话处理。
if (hasRealBearerToken(req)) {
return authenticateKeycloak()
}
if (await authenticateSession()) return
return authenticateKeycloak()
}
}
export function requireAdmin(): RequestHandler {
return requirePermission(PERMISSIONS.SYSTEM_SETTINGS)
}
export function requirePermission(permission: Permission): RequestHandler {
return function permissionGuard(_req: Request, res: Response, next: NextFunction) {
const permissions = res.locals.user?.permissions
if (Array.isArray(permissions) && permissions.includes(permission)) {
return next()
}
return res.status(403).json({ code: 403, message: '权限不足', data: null })
}
}
export function requireAnyPermission(...required: Permission[]): RequestHandler {
return function permissionGuard(_req: Request, res: Response, next: NextFunction) {
const permissions = res.locals.user?.permissions
if (Array.isArray(permissions) && required.some((permission) => permissions.includes(permission))) {
return next()
}
return res.status(403).json({ code: 403, message: '权限不足', data: null })
}
}