validateUser($data['account'], $data['password']); $system = $this->validateSystem($data['system_code']); $this->validateSystemPermission($user, $system); $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); // 记录 token 到 Redis 和 MySQL $this->recordUserToken($user->id, $token, $data['system_code']); return [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'expires_in' => config('jwt.jwt_expiration'), 'payload' => $payload, ]; } /** * 飞书登录 * 如果用户不存在则新增用户信息 */ public function feishuLogin(array $data, bool $isLogin = true): array { $feishuUserInfo = $this->getFeishuUserInfo($data); $user = $this->getOrCreateUser($feishuUserInfo); $system = $this->validateSystem($data['system_code']); if ($user->isLogin === true) { $this->validateSystemPermission($user, $system); } $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); // 记录 token 到 Redis 和 MySQL $this->recordUserToken($user->id, $token, $data['system_code']); return [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'expires_in' => config('jwt.jwt_expiration'), 'payload' => $payload, ]; } /** * 扫码登录 */ public function qrCodeLogin(array $data): array { $userId = $this->validateQrCodeToken($data['qr_code_token']); $user = User::find($userId); if (!$user || $user->status != 1) { throw new BusinessException('用户无效', 403); } $system = $this->validateSystem($data['system_code']); $this->validateSystemPermission($user, $system); $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); // 记录 token 到 Redis 和 MySQL $this->recordUserToken($user->id, $token, $data['system_code']); return [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'expires_in' => config('jwt.jwt_expiration'), 'payload' => $payload, ]; } /** * 注册 */ public function register(array $data) { // 获取可用用户编号 $sn = $this->getAvailableUserSn(); $data['sn'] = $sn->user_sn; // 对密码进行哈希处理 $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); // 获取企业ID $corpInfo = $this->validateCorp($data['corp_abbr']); $data['corp_id'] = $corpInfo->id; // 创建用户记录 $user = $this->createUser($data); // 更新用户编号的用户 ID $this->updateUserId($user->id, $sn->user_sn); // 注册后如果直接登录(返回token) $system = $this->validateSystem($data['system_code']); $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); $this->recordUserToken($user->id, $token, $data['system_code']); return [ 'user' => $user, 'access_token' => $token, 'refresh_token' => $refreshToken, 'expires_in' => config('jwt.jwt_expiration'), 'payload' => $payload, ]; } public function updateUserId($userId, $userSn) { $updated = UserSn::where('user_sn', $userSn) ->update(['user_id' => $userId]); if ($updated === 0) { throw new BusinessException('用户编号不存在或更新失败'); } return $updated; } /** * 获取可用的用户编号 * * @return \app\common\model\UserSn 用户编号模型实例 * @throws BusinessException 当未找到适用编号时抛出 */ private function getAvailableUserSn(): \app\common\model\UserSn { $userSnModel = new UserSn(); $sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first(); if (!$sn) { Log::error('用户注册时未找到适用编号'); throw new BusinessException('未找到适用编号'); } return $sn; } /** * 创建用户记录 * * @param array $data 包含用户信息的数组 * @return \app\common\model\User 用户模型实例 */ private function createUser(array $data): \app\common\model\User { try { return User::create($data); } catch (\Throwable $e) { Log::error('用户注册时创建用户记录失败', [ 'data' => $data, 'error' => $e->getMessage() ]); throw new BusinessException('用户注册失败,请稍后重试', 500); } } /** * 刷新token * @param string $refreshToken * @param string|null $systemCode 可选,指定系统 * @return array */ public function refreshToken(string $refreshToken, ?string $systemCode = null): array { if (!$systemCode) throw new BusinessException('缺少system_code', 422); // 校验refresh_token $userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}"); if (!$userId) { throw new BusinessException('refresh_token无效或已过期', 401); } $validToken = Redis::get("refresh_token:{$userId}:{$systemCode}"); if ($validToken !== $refreshToken) { throw new BusinessException('refresh_token无效或已过期', 401); } $user = User::find($userId); if (!$user || $user->status != 1) { throw new BusinessException('用户无效', 403); } $system = $this->validateSystem($systemCode); $this->validateSystemPermission($user, $system); $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $this->recordUserToken($user->id, $token, $systemCode); return [ 'access_token' => $token, 'expires_in' => config('jwt.jwt_expiration'), 'payload' => $payload, ]; } /** * 记录用户token到 Redis 和 MySQL */ protected function recordUserToken(int $userId, string $token, string $systemCode) { $request = request(); $ip = $request ? $request->getRealIp() : ''; $ua = $request ? $request->header('user-agent', '') : ''; $now = time(); $expireAt = $now + config('jwt.jwt_expiration'); // Redis Set 用户token集合 Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token); Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration')); // Redis Hash token元信息 $meta = [ 'system_code' => $systemCode, 'login_ip' => $ip, 'user_agent' => $ua, 'login_time' => $now, 'expired_at' => $expireAt, ]; Redis::hMSet("user_token_meta:{$token}", $meta); Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration')); // 可选:MySQL 持久化 try { UserTokenLog::create([ 'user_id' => $userId, 'token' => $token, 'system_code'=> $systemCode, 'login_ip' => $ip, 'user_agent' => $ua, 'login_time' => $now, 'expired_at' => $expireAt, 'is_valid' => 1, ]); } catch (\Throwable $e) { Log::error('Token log write failed: '.$e->getMessage()); } } /** * 校验token有效性 */ public function verifyToken(string $token, string $systemCode): array { if (!$systemCode) throw new BusinessException('缺少system_code', 422); $payload = (new Jwt())->verifyToken($token); // 校验黑名单 if (Redis::has("token_blacklist:{$token}")) { throw new BusinessException('token已失效', 403); } // 强制校验aud(系统隔离) if ($systemCode && $payload['aud'] != $systemCode && $payload['aud'] != ($payload['system_code'] ?? '')) { throw new BusinessException('token不属于当前系统', 403); } return (array)$payload; } /** * 登出/失效token */ public function logout(?string $token, ?string $systemCode = null) { if ($token) { Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); $payload = null; try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} $userId = $payload['uid'] ?? null; $aud = $payload['aud'] ?? null; $sc = $systemCode ?? $aud; if ($userId && $sc) { Redis::sRem("user_tokens:{$userId}:{$sc}", $token); } Redis::del("user_token_meta:{$token}"); UserTokenLog::where('token', $token)->update(['is_valid'=>0]); } } /** * 强制拉黑token */ public function blockToken(string $token, ?string $systemCode = null) { if (!$token) { throw new BusinessException('token不能为空', 422); } Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); $payload = null; try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} $userId = $payload['uid'] ?? null; $aud = $payload['aud'] ?? null; $sc = $systemCode ?? $aud; if ($userId && $sc) { Redis::sRem("user_tokens:{$userId}:{$sc}", $token); } Redis::del("user_token_meta:{$token}"); UserTokenLog::where('token', $token)->update(['is_valid'=>0]); } /** * 获取用户所有活跃token及元信息(按system_code) */ public function getUserTokens(int $userId, string $systemCode): array { $tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: []; $result = []; foreach ($tokens as $token) { $meta = Redis::hGetAll("user_token_meta:{$token}"); $result[] = [ 'token' => $token, 'meta' => $meta, ]; } return $result; } /** * 强制某用户所有token全部下线 */ public function blockAllTokensOfUser(int $userId, string $systemCode) { $tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: []; foreach ($tokens as $token) { $this->blockToken($token, $systemCode); } } /** ----------- 内部方法 ------------- **/ /** * 用户验证 */ private function validateUser(string $account, string $password) { $user = User::byAccount($account)->first(); if (!$user || !password_verify($password, $user->password)) { throw new BusinessException('账号或密码错误', 401); } if ($user->status != 1) { throw new BusinessException('账号已禁用', 403); } return $user; } /** * 系统验证 */ private function validateSystem(string $systemCode) { $system = Systems::where('code', $systemCode)->first(); if (!$system) { throw new BusinessException('系统不存在', 403); } return $system; } /** * 公司验证 */ private function validateCorp(string $corpAbbr) { $corp = Corp::where('abbr', $corpAbbr)->first(); if (!$corp) { throw new BusinessException('公司不存在', 403); } return $corp; } /** * 飞书应用信息 */ private function validateFeishuApp(int $corpId, string $systemCode) { $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first(); if (!$feishuAppInfo) { throw new BusinessException('飞书应用不存在', 403); } return $feishuAppInfo; } /** * 用户系统权限校验 */ private function validateSystemPermission(User $user, Systems $system) { if ($system->is_role) { $systemIds = $user->systems->pluck('id')->toArray(); if (!in_array($system->id, $systemIds)) { throw new BusinessException('无该系统权限', 403); } } } /** * JWT payload 构建 * 可扩展 payload 字段 */ private function buildJwtPayload(User $user, ?Systems $system = null): array { $now = time(); $exp = $now + config('jwt.jwt_expiration'); $system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : []; $role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : []; return [ 'sub' => 'user_' . $user->id, 'aud' => $system->code, 'iat' => $now, 'nbf' => $now, 'exp' => $exp, 'typ' => 'access', 'uid' => $user->id, 'role_ids' => $role_ids, 'system_ids' => $system_ids ]; } /** * JWT签发 */ private function generateAccessToken(array $payload) { return (new Jwt())->generateToken($payload); } /** * 刷新令牌生成 */ private function generateRefreshToken(int $userId, string $systemCode) { $refreshToken = bin2hex(random_bytes(32)); // user_id+system_code只允许一个refresh_token生效 Redis::set("refresh_token:{$userId}:{$systemCode}", $refreshToken, config('jwt.jwt_refresh_token_expiration')); // 建立refresh_token到user_id+system_code映射,便于校验 Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration')); return $refreshToken; } /** * 验证扫码令牌 */ private function validateQrCodeToken(string $qrCodeToken) { $userId = Redis::get("qr_code_token:{$qrCodeToken}"); if (!$userId) { throw new BusinessException('扫码令牌无效或已过期', 401); } return $userId; } /** * 获取飞书用户信息 */ private function getFeishuUserInfo(array $data) { // 获取凭证 $certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']); // 获取token $feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); $userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']); // 获取用户信息 $userInfo = $feishu->userInfo($userAccessToken['access_token']); if ($userInfo['code'] != 0) { throw new BusinessException('授权失败,请重试'); } return $userInfo['data'] + $data + $certificate; } /** * 查找或创建飞书用户 */ private function getOrCreateUser(array $feishuUserInfo) { $userFeishuApp = UserFeishuApp::firstOrCreate([ 'name' => $feishuUserInfo['en_name'], 'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), 'union_id' => $feishuUserInfo['union_id'], 'user_id' => $feishuUserInfo['user_id'], 'open_id' => $feishuUserInfo['open_id'], 'tenant_key' => $feishuUserInfo['tenant_key'] ]); if ($userFeishuApp->uid) { $user = User::find($userFeishuApp->uid); $user->isLogin = true; return $user; } else { $user = User::where('phone', $userFeishuApp->mobile)->first(); $user->isLogin = true; if (!$user) { $user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp); $user->isLogin = false; } $userFeishuApp->uid = $user->id; $userFeishuApp->corp_id = $feishuUserInfo['corp_id']; if (!$userFeishuApp->save()) { throw new BusinessException('绑定用户飞书记录失败'); } return $user; } } /** * 过滤和格式化手机号 */ private function sanitizePhone($mobile) { return isset($mobile) ? str_replace('+86', '', $mobile) : ''; } /** * 创建飞书用户 */ private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp) { // 1. 获取适用的用户编号 $userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first(); if (!$userSn) { throw new BusinessException('未找到适用编号'); } // 2. 创建用户 $user = new User(); $user->sn = $userSn->user_sn; $user->name = $feishuUserInfo['en_name']; $user->avatar = $feishuUserInfo['avatar_middle'] ?? ''; $user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT); $user->phone = $userFeishuApp->mobile; $user->email = ''; $user->system_code = $feishuUserInfo['system_code']; $user->corp_id = $feishuUserInfo['corp_id']; if (!$user->save()) { throw new BusinessException('用户创建失败'); } // 更新使用状态 $this->updateUserId($user->id, $userSn->user_sn); return $user; } /** * 飞书应用信息 */ public function feishuAppInfo(string $corpAbbr, string $systemCode) { $corpInfo = $this->validateCorp($corpAbbr); $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode); return [ 'app_id' => $fsAppInfo->app_id, 'app_secret' => $fsAppInfo->app_secret, 'r_url' => $fsAppInfo->r_url, 'scope' => $fsAppInfo->scopes, 'state' => $fsAppInfo->state, 'abbr' => $corpAbbr, 'corp_id' => $corpInfo->id ]; } }