HTTP 미들웨어
App Route stack, 등록 별칭, 인자, 요청 효과, 거부 코드, 호스트 전용 경계를 정확히 찾아봅니다.
HTTP 미들웨어
생성된 외부 route stack을 유지하고 동작에 필요한 권한 가드를 추가하세요. 최소 예시에서 순서를, alias 표에서 인자와 거부 응답을 확인할 수 있습니다. 조직 문맥은 좌표를 제공하고 권한 가드는 행위자의 실행 가능 여부를 판단합니다.
시그니처
App은 App\Http\Middleware를 import하지 않습니다. 패키지 경계를 건너는
안정된 이름은 packages/app-sdk/packages/laravel/src/Http/Middleware.php에서 SDK가 공개합니다.
final class Middleware
{
public const AGENT_DELEGATION = 'agent.delegation';
public const APP_INSTALLED = 'app.installed';
public const AUTHENTICATED_LOCALE = 'nexia.locale';
public const LEGAL_ENTITY_CONTEXT = 'nexia.context.legal_entity';
public const WORKSPACE_CONTEXT = 'nexia.context.workspace';
}인자가 있는 권한·구조 guard는 호스트가 등록한 문자열 시그니처입니다. 이 문서에 있는 시그니처만 사용하세요. Core에 미들웨어 클래스가 존재한다는 사실은 그 클래스를 App 의존성으로 만들지 않습니다.
최소 예시
다음은 stubs/package-app/empty-routes.stub이 만드는 바깥 stack에 실제 App
키를 넣은 전체 형태입니다. 테넌트를 찾고 세션을 격리한 다음, 사람 또는
위임된 Agent를 인증하고, 위임 권한을 좁히고, App 가용성을 확인한 뒤 화면과
조직 컨텍스트를 설정합니다.
use Illuminate\Support\Facades\Route;
use Nexia\Http\Middleware;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Middleware\ScopeSessions;
Route::middleware([
'web',
InitializeTenancyByDomainOrSubdomain::class,
PreventAccessFromUnwantedDomains::class,
ScopeSessions::class,
'auth:sanctum,agent',
'agent.delegation',
'app.installed:workshop',
Middleware::LEGAL_ENTITY_CONTEXT,
Middleware::WORKSPACE_CONTEXT,
])->group(function (): void {
// Workshop 경로를 여기에 등록합니다.
});그룹 안의 각 연산을 인가하세요. 생성된 tenant 경로는 can.tenant_wide를 사용합니다. 정식 Legal Entity 경로는 Controller에서 요청 대상 또는 저장된 소유자를 해석해 인가하고, URL에 대상이 있는 경로는 can.scope를 사용합니다. 바깥 stack은 신원과 문맥만 확립합니다.
인자
생성되는 바깥 stack
실행 순서도 계약의 일부입니다. 테넌트를 식별하기 전에 인증을 안전하게 평가할 수 없고, 위임 요청은 capability guard나 controller가 실행되기 전에 먼저 좁혀야 합니다.
| 항목 | 인자 | 필수 | 런타임 효과 |
|---|---|---|---|
web | 없음 | 예 | 브라우저·세션 stack을 시작합니다. Core는 유효한 support session 재검사를 추가하고 기본 CSRF 미들웨어를 bearer-aware 하위 클래스로 교체합니다. |
InitializeTenancyByDomainOrSubdomain::class | 없음 | 예 | 요청 host에서 테넌트를 찾아 해당 database, cache, filesystem, URL 컨텍스트로 진입합니다. |
PreventAccessFromUnwantedDomains::class | 없음 | 예 | 해석된 테넌트에 유효하지 않은 도메인을 거부합니다. |
ScopeSessions::class | 없음 | 예 | 한 테넌트 세션이 다른 테넌트 신원에서 재사용되지 않게 합니다. |
auth:sanctum,agent | guard 목록 | 예 | Sanctum 사람 세션 또는 서명된 Agent 위임 토큰을 받습니다. 인증만으로는 아무 권한도 생기지 않습니다. |
Middleware::AGENT_DELEGATION | 없음 | Agent 호출 가능 Route에 필수 | 위임 요청에서 토큰 allowlist, 서명된 Legal Entity·Operating Unit 대상, 실시간 도구 정책의 교집합을 적용합니다. 사람 요청에서는 아무 일도 하지 않습니다. |
Middleware::APP_INSTALLED.':{app_key}' | app_key | 예 | App과 모든 전이 prerequisite가 테넌트에서 operational인지 확인합니다. |
Middleware::LEGAL_ENTITY_CONTEXT | 고정 Route 이름 legalEntity | 생성 stack에 필수 | 명시적인 Route·위임 대상을 쓰고, 없으면 유효한 세션·기본 선호를 복원합니다. 권한을 부여하지 않습니다. |
Middleware::WORKSPACE_CONTEXT | 없음 | 생성 stack에 필수 | 접근 가능한 Workspace 선호를 복원하거나 지웁니다. Workspace는 화면 상태이며 권한 범위가 아닙니다. |
등록된 Nexia 별칭
bootstrap/app.php는 커스텀 별칭 열네 개를 등록합니다. 열 개는 App
Route에서 사용하고 네 개는 플랫폼 전용 진입점에 속합니다.
| 별칭 시그니처 | 인자와 기본값 | 사용 주체 | 동작 |
|---|---|---|---|
agent.delegation | 없음 | App·Core API 그룹 | 위임된 Agent 권한을 좁힙니다. 사람의 Sanctum 요청은 그대로 통과합니다. |
app.installed:{app_key} | 필수 App 키 | App Route | App prerequisite closure 전체가 operational이 아니면 404를 반환합니다. |
nexia.locale | 없음 | 현지화된 App Route | 인증된 사용자의 요청 유효 locale을 해석합니다. 인증 뒤에 배치하며 표시 언어만 선택하고 권한은 부여하지 않습니다. |
nexia.context.legal_entity | 고정 legalEntity Route 인자 | App·Core Route | 해석한 내부 ID를 request attribute legal_entity_id에 둡니다. 선택과 membership은 권한이 아닌 컨텍스트입니다. |
nexia.context.workspace | 미들웨어 인자 없음 | App·Core 브라우저 Route | 사용자가 접근할 수 있을 때만 세션 workspace_id를 유지합니다. |
can.tenant_wide:{permission} | 필수 권한 slug | App·Core 연산 | 빈 테넌트 전체 데이터 대상을 기준으로 권한을 평가합니다. 더 좁은 Legal Entity·Operating Unit Grant로는 통과하지 못합니다. |
can.scope:{permission},{scope_type},{route_parameter}[,{subject_population}] | 권한, 차원 키, Route 인자, self 같은 선택적 SDK population 값 | App·Core 연산 | 명시적인 공개 Resource 대상을 해석하고 Access Grant를 평가하며, 해당하면 Legal Entity·Operating Unit request attribute를 기록합니다. |
operating_unit.affiliated[:{legalEntityParameter},{operatingUnitParameter}] | 기본값 legalEntity, operatingUnit | 중첩된 App·Core Route | 해석된 두 Route Resource의 affiliation을 증명합니다. 이 구조 검사는 Grant를 만들지 않습니다. |
nexia.context.operating_unit | 고정 operatingUnit Route 인자 | 명시적 Operating Unit을 가진 App·Core Route | request attribute operating_unit_id를 기록합니다. 위임 요청은 서명된 대상과 일치해야 합니다. SDK에 Middleware 상수가 없으므로 App Route는 현재 호스트 등록 문자열을 직접 사용합니다. |
human.only | 없음 | 사람 셀프서비스 App·Core Route | 위임 Agent 표식을 거부하고 인증된 사람 경로만 계속 실행합니다. |
service-token | X-Service-Token header, 미들웨어 인자 없음 | Core /internal/* 전용 | 설정된 service secret으로 Agent gateway를 인증합니다. App Route 계약이 아닙니다. |
agent_data:{action} | 기본값 read, *이면 요청에서 action을 읽음 | Core 범용 Agent-data endpoint 전용 | 런타임 Resource 키를 해석하고 위임 allowlist와 실시간 권한을 강제합니다. App은 Resource를 contribution하지만 이 별칭을 붙이지 않습니다. |
signature.enabled | 없음 | Core Signature 진입점 전용 | 테넌트 기능이 꺼져 있으면 플랫폼 Signature 표면을 거부합니다. App Route 계약이 아닙니다. |
tenant.require_2fa | 없음 | Tenant Shell Route 전용 | 등록 화면을 위해 document Shell은 열어 두고, 아직 등록이 필요한 JSON 요청에는 423을 반환합니다. |
옵션: guard 선택과 순서
호출하는 화면이 아니라 실제 연산의 데이터 소유자를 기준으로 capability guard를 고릅니다.
| 상황 | 바깥 stack 다음에 추가 | 이유 |
|---|---|---|
| 테넌트 소유 Catalog·Setting | can.tenant_wide:quality.inspection_method.read | 더 좁은 Resource 좌표가 없으므로 테넌트 전체 Grant만 적용됩니다. |
| URL에 Legal Entity 대상이 있는 경로 | can.scope:quality.inspection_result.read,core.legal_entity,legalEntity | URL이 Access Grant 판단에 쓸 정확한 Legal Entity 대상을 제공합니다. |
| Operating Unit 소유 레코드 | operating_unit.affiliated 다음 can.scope:{permission},core.operating_unit,operatingUnit | affiliation은 Route 구조를, scoped guard는 권한을 별도로 증명하며 Legal Entity 좌표도 요구합니다. |
| Agent가 절대 호출하면 안 되는 셀프서비스 데이터 | capability guard보다 먼저 human.only | 사람 전용 조건은 호출자 종류를 좁힐 뿐 권한을 대신하지 않습니다. |
| 한 population으로 제한된 권한 | self 같은 네 번째 can.scope 인자 | 동작과 데이터 범위뿐 아니라 그 population도 결정에서 허용되어야 합니다. |
app.installed는 개별 CRUD Route 바깥에 두어 App endpoint 전체가 같은 방식으로
닫히게 합니다. can.*는 가장 작은 구체 연산에 둡니다. Policy는 로드된
레코드를 다시 검사하고, 목록 술어는 범위 밖 행을 SQL에서 제외하며, 도메인
서비스는 전이 규칙을 강제합니다. 미들웨어는 어느 층도 대신하지 않습니다.
결과 또는 반환
통과한 미들웨어는 다음 응답을 반환합니다. 컨텍스트 미들웨어는 downstream 계약에 요청 로컬 값을 추가할 수 있지만, 그 값 자체는 권한의 증거가 아닙니다.
| 효과 | 소비자 | 보안 의미 |
|---|---|---|
| 활성 테넌트와 tenant database connection | Model, cache, filesystem, URL 생성 | App 코드보다 먼저 격리 경계가 확립됨 |
| 인증된 사람 또는 위임된 Agent | Controller와 권한 서비스 | 행위자 신원일 뿐임 |
| 검증된 브라우저 password session | 세션 검사가 붙은 Core API 루트 경로 | Password 변경으로 무효가 된 session은 거부하며 stateless 위임 요청은 이 호스트 검사를 그대로 통과 |
legal_entity_id request attribute | 권한 컨텍스트와 downstream 서비스 | 명시적·선택 컨텍스트이며 Grant가 아님 |
operating_unit_id request attribute | Operating Unit 인식 권한·서비스 | 명시적 Route·위임 컨텍스트이며 affiliation이나 권한이 아님 |
세션 workspace_id | Shell과 브라우저 화면 복원 | 화면 컨텍스트일 뿐임 |
can.* 권한 판단 | 현재 연산 | 거친 capability 판단이며 레코드·상태 검사는 이후에도 실행됨 |
생성된 App 경로는 아래 순서로 처리합니다. 인가는 경로 guard 또는 Controller에서 수행합니다.
tenant 식별
-> tenant 격리 session
-> 사람 또는 Agent 인증
-> 위임 권한 축소
-> App operational 검사
-> request context 설정
-> Route capability guard
-> Policy와 목록 술어
-> 도메인 전이
Core의 routes/api.php 루트 그룹에는 권한 거부 감사, throttle:api, 비밀번호 세션 검사가 명시적으로 붙습니다. 패키지 경로는 자체 provider에서 로드하므로 URL이 /api로 시작해도 이 그룹을 상속하지 않습니다. 필요한 rate limit은 경로에 명시하세요.
공유 api limiter는 일반 traffic을 tenant와 user id 또는 IP로 묶어 분당 300회
허용합니다. 위임 Agent traffic은 tenant와 Agent id로 묶은 별도 분당 240회
bucket을 사용하므로 사용자 browser bucket을 소모하지 않습니다. 더 엄격한
endpoint별 limit은 그 위에 계속 적용됩니다. API root는 stateful web guard로
Laravel password-hash session 검사도 실행하며, stateless 위임 traffic은 그대로
통과합니다. web과 api 그룹은 교환된 support session도 매 요청마다 다시
확인합니다. 이 자동 호스트 concern은 App이 패키지 Route 파일로 복사하는
별칭이 아닙니다.
오류
| 관찰 결과 | 원인 | 해결 |
|---|---|---|
401 | 인증 실패, password 변경으로 브라우저 session 무효화, 또는 플랫폼 service-token 부재·불일치 | 지원되는 사람·Agent 자격으로 다시 인증합니다. App에서 내부 service token을 사용하지 마세요. |
agent.delegation의 403 | 권한 slug, 서명된 조직 대상, 도구 정책이 위임 범위 밖 | 올바르게 좁힌 토큰을 만들고 Route의 can.* 선언을 실제 capability와 맞춥니다. |
can.tenant_wide 또는 can.scope의 403 | 적용 가능한 Access Grant, 대상 차원, 요청 population이 없음 | Grant 또는 Route의 실제 데이터 좌표를 고칩니다. Shell 선택으로 대체하지 마세요. |
human.only의 403 | 위임 Agent가 사람 셀프서비스 endpoint 호출 | 명시적 권한이 있는 Agent 가능 연산을 사용하거나 endpoint를 사람 전용으로 유지합니다. |
app.installed의 404 | App이 없거나 disabled·미초기화·unknown 상태이거나 prerequisite가 막힘 | App closure를 설치·복구합니다. guard를 우회하지 마세요. |
operating_unit.affiliated의 404 | Route의 Legal Entity와 Operating Unit이 affiliated 상태가 아님 | 두 안정 공개 Route 식별자를 바로잡습니다. |
419 | 교환된 support session이 만료되었거나 중앙에서 회수됨 | 가장 세션을 끝내고 승인된 support session을 새로 시작합니다. |
JSON 423 | 테넌트 정책상 현재 비밀번호 세션에 2FA 등록 필요 | 등록을 완료합니다. SSO 정책은 외부 IdP가 계속 소유합니다. |
429 | 공유 API bucket 또는 더 엄격한 endpoint별 limiter 소진 | 응답 rate-limit header를 따르고 backoff합니다. 호스트 limiter를 우회하지 마세요. |
service-token의 503 | 플랫폼 service-token 설정이 비어 있음 | 내부 gateway 양쪽을 설정합니다. App fallback이 아니라 운영 문제입니다. |
실제 사용
packages/quality/routes/routes.php는 테넌트 초기화, 인증, App 설치, Legal Entity 문맥, Workspace 문맥을 처리하는 생성된 외부 그룹을 유지합니다. Resource 경로를 추가할 때 동작에 맞는 권한 가드를 붙이세요.
packages/people/routes/routes.php는 URL이 두 조직 좌표를 운반하는 곳에
nexia.context.operating_unit과 operating_unit.affiliated를 추가합니다.
packages/payroll/routes/routes.php는 개인 급여 명세에 human.only와 네 번째
인자가 self인 can.scope를 함께 적용합니다.
상태를 바꾸지 않고 등록된 chain을 확인할 수 있습니다.
task artisan -- route:list --path=quality
task artisan -- route:list --path=legal-entities --json집중 동작 근거는
tests/Feature/AppRuntime/TenantAppInstallationRuntimeTest.php,
tests/Feature/Authorization/CanTenantWideMiddlewareTest.php,
tests/Feature/Access/RouteOperatingUnitContextTest.php,
tests/Feature/Agent/AgentDelegationGuardTest.php,
tests/Feature/WorkspaceContextTest.php에 있습니다.
관련 문서
- SDK 계약 — Core 클래스를 노출하지 않고 미들웨어 이름을 공개하는 PHP 경계
- 권한과 역할 —
can.*뒤에서 capability, Grant 범위, population이 판단을 만드는 방식 - 보호된 API 추가하기 — 한 App 연산에 guard, Policy, 목록 술어, 거부 테스트 적용
- 테넌트와 컨텍스트 — guard를 고르기 전에 tenant, actor, 조직, Workspace 컨텍스트 구분
- Agent Resource Data 런타임 — 일반 Agent 쓰기가 같은 권한 적용 App Route로 돌아오는 방식