Files
Gnuboard7/app/Providers/AuthServiceProvider.php
T
2026-04-01 10:30:52 +09:00

291 lines
9.0 KiB
PHP

<?php
namespace App\Providers;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
class AuthServiceProvider extends ServiceProvider
{
/**
* guest role 캐시
*/
protected static ?Role $guestRoleCache = null;
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
// 회원 권한 체크 (비회원은 Gate::before가 호출되지 않으므로 checkPermission() 사용)
// admin 역할도 시더에서 모든 리프 권한을 명시 할당받으므로 별도 바이패스 불필요
Gate::before(function ($user, $ability) {
if ($user) {
// 회원: 기존 hasPermission() 메서드 활용
return $user->hasPermission($ability) ? true : null;
}
// 비회원: Laravel Gate는 guest에 대해 before 콜백을 호출하지 않으므로
// 여기에 도달하지 않음. 대신 checkPermission() 정적 메서드 사용
return null;
});
}
/**
* 회원/비회원 통합 권한 체크 메서드
*
* Laravel Gate::before가 guest에게 호출되지 않는 문제를 해결하기 위해
* Gate와 직접 guest 권한 체크를 조합합니다.
*
* @param string $ability 권한 식별자
* @param User|null $user 사용자 (null이면 현재 인증된 사용자 또는 guest)
* @return bool 권한 허용 여부
*/
public static function checkPermission(string $ability, ?User $user = null): bool
{
// 사용자 결정: 전달된 사용자 > 현재 인증 사용자 > guest
$user = $user ?? Auth::user();
if ($user) {
// 인증된 사용자: Gate 사용 (before 콜백 포함)
return Gate::forUser($user)->allows($ability);
}
// 비회원: 직접 guest role 권한 체크
return self::checkGuestPermission($ability) === true;
}
/**
* 여러 권한을 한 번에 체크합니다. (AND 조건)
*
* @param array<string> $abilities 권한 식별자 배열
* @param User|null $user 사용자 (null이면 현재 인증된 사용자 또는 guest)
* @return bool 모든 권한이 허용되면 true
*/
public static function checkPermissions(array $abilities, ?User $user = null): bool
{
foreach ($abilities as $ability) {
if (! self::checkPermission($ability, $user)) {
return false;
}
}
return true;
}
/**
* 구조화된 권한 로직(OR/AND)을 지원하는 권한 체크 메서드
*
* 지원 형식:
* - flat array: ["a", "b"] → AND (기존 호환)
* - OR 객체: {"or": ["a", "b"]} → a 또는 b
* - AND 객체: {"and": ["a", "b"]} → a 그리고 b
* - 중첩: {"and": ["a", {"or": ["b", "c"]}]} → a + (b 또는 c)
*
* @param array|object $permissions 권한 구조 (flat array 또는 구조화 객체)
* @param User|null $user 사용자 (null이면 현재 인증된 사용자 또는 guest)
* @param int $depth 현재 재귀 깊이 (내부용)
* @return bool 권한 충족 여부
*/
public static function checkPermissionsWithLogic(array|object $permissions, ?User $user = null, int $depth = 1): bool
{
// 최대 중첩 깊이 초과 시 거부
if ($depth > 3) {
Log::warning('권한 구조 최대 중첩 깊이(3) 초과', [
'permissions' => $permissions,
'depth' => $depth,
]);
return false;
}
// 배열 또는 객체를 배열로 변환
$permissions = (array) $permissions;
// 빈 배열 → 권한 불필요
if (empty($permissions)) {
return true;
}
// flat array (기존 호환): 모든 문자열 → AND 로직
if (array_is_list($permissions)) {
return self::evaluateList($permissions, $user, $depth);
}
// 구조화 객체: {"or": [...]} 또는 {"and": [...]}
$keys = array_keys($permissions);
if (count($keys) !== 1 || ! in_array($keys[0], ['or', 'and'])) {
Log::warning('잘못된 권한 구조 연산자', [
'keys' => $keys,
'permissions' => $permissions,
]);
return false;
}
$operator = $keys[0];
$items = $permissions[$operator];
if (! is_array($items) || ! array_is_list($items)) {
return false;
}
if ($operator === 'or') {
return self::evaluateOr($items, $user, $depth);
}
return self::evaluateAnd($items, $user, $depth);
}
/**
* 리스트 내 항목을 AND 로직으로 평가합니다.
*
* @param array $items 권한 항목 배열 (문자열 또는 중첩 구조)
* @param User|null $user 사용자
* @param int $depth 현재 재귀 깊이
* @return bool 모든 항목이 충족되면 true
*/
private static function evaluateList(array $items, ?User $user, int $depth): bool
{
foreach ($items as $item) {
if (! self::evaluateItem($item, $user, $depth)) {
return false;
}
}
return true;
}
/**
* OR 로직으로 평가합니다.
*
* @param array $items 권한 항목 배열
* @param User|null $user 사용자
* @param int $depth 현재 재귀 깊이
* @return bool 하나라도 충족되면 true
*/
private static function evaluateOr(array $items, ?User $user, int $depth): bool
{
foreach ($items as $item) {
if (self::evaluateItem($item, $user, $depth)) {
return true;
}
}
return false;
}
/**
* AND 로직으로 평가합니다.
*
* @param array $items 권한 항목 배열
* @param User|null $user 사용자
* @param int $depth 현재 재귀 깊이
* @return bool 모든 항목이 충족되면 true
*/
private static function evaluateAnd(array $items, ?User $user, int $depth): bool
{
foreach ($items as $item) {
if (! self::evaluateItem($item, $user, $depth)) {
return false;
}
}
return true;
}
/**
* 단일 항목을 평가합니다.
*
* 문자열이면 직접 권한 체크, 배열이면 재귀적으로 구조 평가.
*
* @param mixed $item 권한 항목 (문자열 또는 중첩 구조)
* @param User|null $user 사용자
* @param int $depth 현재 재귀 깊이
* @return bool 권한 충족 여부
*/
private static function evaluateItem(mixed $item, ?User $user, int $depth): bool
{
if (is_string($item)) {
return self::checkPermission($item, $user);
}
if (is_array($item)) {
return self::checkPermissionsWithLogic($item, $user, $depth + 1);
}
return false;
}
/**
* 비회원(guest) role의 권한을 확인합니다.
*
* @param string $ability 권한 식별자
* @return bool|null 권한 있으면 true, 없으면 null (다음 Gate로 위임)
*/
public static function checkGuestPermission(string $ability): ?bool
{
$guestRole = self::getGuestRole();
if (! $guestRole) {
return null; // guest role 없으면 다음 Gate 정의로 위임
}
return $guestRole->permissions()
->where('identifier', $ability)
->exists() ? true : null;
}
/**
* guest role을 캐싱하여 조회합니다.
*/
protected static function getGuestRole(): ?Role
{
if (self::$guestRoleCache === null) {
self::$guestRoleCache = Role::where('identifier', 'guest')
->with('permissions')
->first();
}
return self::$guestRoleCache;
}
/**
* guest role 캐시를 초기화합니다. (테스트용)
*/
public static function clearGuestRoleCache(): void
{
self::$guestRoleCache = null;
}
/**
* 테스트용: 비회원 권한 체크 결과를 직접 확인합니다.
*
* @param string $ability 권한 식별자
* @return array{result: bool|null, guestRole: Role|null, hasPermission: bool}
*/
public static function debugCheckGuestPermission(string $ability): array
{
$guestRole = self::getGuestRole();
$hasPermission = false;
if ($guestRole) {
$hasPermission = $guestRole->permissions()
->where('identifier', $ability)
->exists();
}
return [
'result' => self::checkGuestPermission($ability),
'guestRole' => $guestRole,
'hasPermission' => $hasPermission,
'cachedRoleId' => self::$guestRoleCache?->id,
];
}
}