diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php new file mode 100644 index 0000000..ec8da32 --- /dev/null +++ b/app/common/logic/AuthLogic.php @@ -0,0 +1,602 @@ +validateUser($data['account'], $data['password']); + $system = $this->validateSystem($data['system_code']); + $this->validateSystemPermission($user, $system); + + return $this->generateTokensAndReturn($user, $system, $data); + } + + /** + * 飞书登录 + * 如果用户不存在则新增用户信息 + */ + public function feishuLogin(array $data): array + { + $feishuUserInfo = $this->getFeishuUserInfo($data); + return $feishuUserInfo; + $user = $this->getOrCreateUser($feishuUserInfo); + + $system = $this->validateSystem($data['system_code']); + if ($user->isLogin) { + $this->validateSystemPermission($user, $system); + } + + return $this->generateTokensAndReturn($user, $system, $data); + } + + /** + * 注册 + */ + public function register(array $data) + { + try { + // 获取可用用户编号 + $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; + + // 处理账户 + $data = $data + normalizeAccount($data['account']); + + // 创建用户记录 + $user = $this->createUser($data); + + // 更新用户编号的用户 ID + $this->updateUserId($user->id, $sn->user_sn); + + $system = $this->validateSystem($data['system_code']); + return $this->generateTokensAndReturn($user, $system, $data); + } catch (\Throwable $e) { + throw $e; + } + } + + /** + * 查找或创建飞书用户 + */ + private function getOrCreateUser(array $feishuUserInfo) + { + try { + $phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''); + $email = $feishuUserInfo['enterprise_email']; + // $user = User::where('corp_id', $feishuUserInfo['corp_id']) + // ->where(function($query) use ($phone, $email) { + // $query->where('phone', $phone) + // ->orWhere('email', $email); + // }) + // ->first(); + $userByPhone = User::where('corp_id', $feishuUserInfo['corp_id']) + ->where('phone', $phone) + ->where('phone', '!=', '') + ->first(); + $userByEmail = User::where('corp_id', $feishuUserInfo['corp_id']) + ->where('email', $email) + ->where('email', '!=', '') + ->first(); + $user = $userByPhone ?: $userByEmail; + if (!$user) { + // 获取编号 + $userSn = $this->getAvailableUserSn(); + // 创建用户 + $user = User::create([ + 'name' => $feishuUserInfo['name'], + 'avatar' => $feishuUserInfo['avatar_middle'], + 'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), + 'email' => $feishuUserInfo['enterprise_email'], + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'], + 'corp_id' => $feishuUserInfo['corp_id'], + 'password' => password_hash($userSn->user_sn, PASSWORD_DEFAULT) + ]); + // 更新使用 + $this->updateUserId($user->id, $userSn->user_sn); + // 更新编号 + if (!$user->update(['sn'=>$userSn->user_sn])) { + throw new BusinessException('编号绑定失败'); + } + // 是否登陆 + $user->isLogin = false; + + $options = [ + 'corp_id' => $user->corp_id, + 'user_id' => $user->id + ]; + \Webman\RedisQueue\Redis::send('perm-agt', $options); + } else { + $updated = [ + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'] + ]; + if ($phone && $phone == $user->phone) { + $updated['phone'] = $phone; + } + if ($phone && $phone == $user->phone) { + $updated['email'] = $email; + } + // 更新飞书信息 + $user->update($updated); + // 是否登陆 + $user->isLogin = true; + } + return $user; + } catch (\Throwable $e) { + throw $e; + } + } + + /** + * 过滤和格式化手机号 + */ + private function sanitizePhone($mobile) + { + return isset($mobile) ? str_replace('+86', '', $mobile) : ''; + } + + /** + * 生成并记录token,返回包含token的响应数据 + */ + private function generateTokensAndReturn(User $user, Systems $system, array $data): array + { + $payload = $this->buildJwtPayload($user, $system); + $token = $this->generateAccessToken($payload); + $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); + $homeUrl = $this->getHome($system->id, $payload); + + // 记录 token 到 Redis 和 MySQL + $this->recordUserToken($user->id, $token, $data['system_code']); + + return [ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'expires_in' => config('jwt.jwt_expiration'), + 'system_code' => $data['system_code'], + 'corp_abbr' => $data['corp_abbr'], + 'login_type' => $data['login_type'], + 'home_url' => $homeUrl, + // 'payload' => $payload, + ]; + } + + /** + * 获取飞书用户信息 + */ + private function getFeishuUserInfo(array $data) + { + $app = [ + 'cli_a8033d607cba500d' => 'OZW2UOeJ0g4HJfXKSn6skcymCOyYIdDR', + ]; + if(empty($app[$data['app_id']])){ + throw new BusinessException('app_id非法'); + } + $fsAppInfo = CorpFeishuApp::where('app_id', $data['app_id'])->first(); + $feishu = new Feishu($data['app_id'], $fsAppInfo['app_secret']); + + //回调地址 + $redirect_uri = $fsAppInfo['r_url']; + + $userAccessToken = $feishu->userAccessToken($data['feishu_code'], $redirect_uri); + $userInfo = $feishu->userInfo($userAccessToken['access_token']); + + if ($userInfo['code'] != 0) { + throw new BusinessException('授权失败,请重试'); + } + + + User::firstOrCreate(['union_id'=>$userInfo['data']['union_id']],[ + 'union_id'=>$userInfo['data']['union_id'], + 'name'=>$userInfo['data']['name'], + 'app_id'=>$data['app_id'] + ]); + return $userInfo['data'] + $data; + } + + /** + * 更新用户编号 + */ + private function updateUserId($userId, $userSn) + { + $updated = UserSn::where('user_sn', $userSn) + ->update(['user_id' => $userId]); + + if ($updated === 0) { + throw new BusinessException('用户编号不存在或更新失败'); + } + + return $updated; + } + + /** + * 记录用户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; + } + + /** + * 获取飞书应用信息 + */ + public function feishuAppInfo(string $app_id, string $systemCode) + { + $fsAppInfo = CorpFeishuApp::where('app_id', $app_id)->first(); + return [ + 'app_id' => $fsAppInfo->app_id, + 'app_secret' => $fsAppInfo->app_secret, + 'r_url' => $fsAppInfo->r_url, + 'scope' => $fsAppInfo->scopes, + 'state' => $fsAppInfo->state, + ]; + } + + /** + * 验证用户信息 + */ + 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, int $systemId) + { + $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->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); + } + } + } + + /** + * 获取可用的用户编号 + * + * @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); + } + } + + /** + * 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, + 'uname' => $user->name, + '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; + } + + + /** + * 刷新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); + $refreshToken = $this->generateRefreshToken($user->id, $systemCode); + $this->recordUserToken($user->id, $token, $systemCode); + + return [ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'expires_in' => config('jwt.jwt_expiration'), + // 'payload' => $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]); + } + + + public function getHome($systemId, $data) + { + // 2025.10.30 临时处理 ====== start + if ($systemId == 1) { + if (in_array(1, $data['role_ids'])) { + return '/dashboard/console'; + } + if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) { + return '/dashboard/console/backoffice'; + } + if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) { + return '/dashboard/console/finance'; + } + } + // 2025.10.30 临时处理 ====== end + + $info = Resource::where('system_id', $systemId) + ->where('is_home', 1) + ->first(); + + if (!$info) { + return '/dashboard/console'; + } + + // 递归拼接 URL + $url = $this->buildParentUrl($info); + + return $url ?: '/dashboard/console'; + } + + /** + * 递归构建父级 URL + * @param Resource $resource + * @return string + */ + protected function buildParentUrl(Resource $resource) + { + $url = $resource->url ?? ''; + + // 如果存在父级,则递归拼接 + if ($resource->parent_id != 0) { + $parent = Resource::find($resource->parent_id); + if ($parent) { + $parentUrl = $this->buildParentUrl($parent); + $url = $parentUrl . ($url ? '/' . ltrim($url, '/') : ''); + } + } + + return $url; + } +} \ No newline at end of file