127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
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 })
|
||
}
|
||
}
|