권한과 로그인
누가 무엇을 할 수 있는지는 엔티티의 meta: 줄에 적고, 다시 생성되는 코어 서비스에서 검사합니다 - 개발자 소유인
컨트롤러가 아니라서, DSL 을 고치면 다음 빌드에 모든 진입점에 반영됩니다. 3.0 규칙 엔진과 jpa-service-access 가 든
번들이 필요합니다: 모든 v3 번들(DDD, 헥사고날, -Pv3, Kotlin). 2.x 생성기는 이 어노테이션들을 처리하지 않습니다.
@access 와 @owner
PurchaseOrder:
meta: entity @root @expose @owner(customerId)
@access(read=owner, create=user, cancel=owner) # ship·delete 는 적지 않음 -> admin
| 수준 | 누가 |
|---|---|
public | 누구나 (로그인 불필요) |
user | 로그인한 사용자 |
owner | 그 행의 주인(@owner 필드 = 로그인 ID)과 관리자 |
admin | 관리자 - 역할 ADMIN, env.yml 의 config: adminRole: 로 변경 |
none | 아무도 (애플리케이션 자신은 가능) |
연산 이름: read(조회·검색), create, update, delete, 그리고 명령 이름(ship, cancel ...). 적지 않은 연산은
admin 입니다(*=user 로 기본값 지정). @access 가 없으면 아무것도 검사하지 않습니다.
| 상황 | 결과 |
|---|---|
로그인 안 함 + user/owner/admin | 401 - 제목 Sign in first, 문장 Sign in to <연산> <엔티티> (예: Sign in to create PurchaseOrder) |
| 로그인했지만 권한 없음 | 403 |
owner: 남의 행 조회·수정·삭제·명령 | 404 - 존재 자체를 알리지 않음 |
owner: 검색 | 소유자 필드의 동등 조건을 덧붙임 - 내 것만 나옴 |
create | 소유자 필드를 로그인 ID 로 - 요청이 무엇을 보냈든 (관리자의 값은 그대로) |
update | 소유자 필드와 @access(admin) 필드는 행의 현재 값으로 |
owner: findAll, 조건 삭제·조건 수정 | 관리자만 |
로그인 안 한 호출 (요청 밖, @Async ...) | 방문자 - fail-closed |
CurrentUser.runAsSystem(() -> ...) 안 | 애플리케이션 자신 - 모두 허용 |
다른 엔티티의 행 전체(중첩 DTO)는 그 엔티티의 읽기 수준이 public 일 때만 DTO 에 실립니다. 아니면 빌드가 그
필드를 빼고 경고합니다(식별자는 남음).
관리자만 쓰는 필드
Customer:
meta: entity @root @editable @expose @owner(customerId) @access(read=owner, create=public, update=owner)
fields:
role: UserRole @default(UserRole.USER) @access(admin) # 가입자가 스스로 관리자가 될 수 없음
다른 사람이 보낸 값은 생성 시 엔티티 기본값으로, 수정 시 행의 현재 값으로 바뀝니다.
@password 와 @login
login: string(10)! @login @unique
password: string(255)! @password # 해시 길이를 담을 만큼 (PBKDF2 약 100자)
- 요청으로 받은 비밀번호는 모든 쓰기 경로에서 해시로만 저장합니다(기본 PBKDF2-SHA256,
PasswordHasher빈으로 교체). 응답·검색·DTO 복사에 나오지 않고, TypeScript 에서는 optional 입니다. @login과@password가 함께 있으면 코어 서비스와 유스케이스에authenticate(login, password)가 생깁니다: 그 행, 또는 로그인이 없거나 두 행 이상이거나 비밀번호가 틀리면 비어 있음. 없는 로그인도 같은 해시 비용을 치러 응답 시간으로 계정 존재를 알 수 없습니다.- 로그인은 유일해야 합니다:
@signin없이 쓰는 엔티티의 로그인이@unique(또는 키, 그 컬럼 하나만의 unique@Index)가 아니면 DSL 경고입니다.
@signin - 로그인 API 생성
Customer:
meta: entity @root @editable @expose(20) @signin @owner(customerId) @access(read=owner, create=public, update=owner)
fields:
customerId: long @id @sequence
login: string(10)! @search(eq) @login
password: string(255)! @password
role: UserRole @default(UserRole.USER) @access(admin)
| 요청 | 답 |
|---|---|
POST /api/shop/customer/signin {"login": "...", "password": "..."} (키는 두 필드의 이름) | 200 고객 (비밀번호 없음), 세션 시작. 틀리면 401 Wrong login or password |
POST /api/shop/customer/signout | 204, 세션 끝 |
GET /api/shop/customer/me | 200 로그인한 고객. 방문자는 401 |
코어 서비스·유스케이스에 me() 가, TypeScript 클라이언트에 signIn(login, password), signOut(), me()(방문자면
null)가 생깁니다. 쿠키 세션이라 화면과 API 가 같은 출처일 때 동작합니다. @login 컬럼에는 unique = true 가 붙어,
이미 있는 로그인은 409 입니다.
세션에는 행의 키와 역할이 들어갑니다. 역할은 @role 을 붙인 필드, 없으면 role 이라는 필드이고, 관리자만 쓰는
필드(@access(admin))인 enum 이나 문자열일 때만 씁니다. enum 이면 상수 이름(ADMIN)을 adminRole 과 비교합니다.
그렇지 않으면 가입자가 "role":"ADMIN" 을 보내고 관리자로 로그인할 수 있으므로, 빌드가 경고하고 역할 없이 로그인합니다.
@signin 은 안전할 때만 생성됩니다 - 아니면 DSL 경고가 이유와 고칠 문장을 알려 줍니다:
- 그 행은 본인과 관리자만 바꿀 수 있어야 합니다:
update=owner와 키를 가리키는@owner, 또는update=admin·none. (@access가 없으면 수정 수준이public이라 아무나 관리자의 비밀번호를 바꿀 수 있습니다.) - 키를 요청이 정하면(
@genid·@sequence가 아님) 삭제도 같게 제한됩니다 - 아니면 누가alice를 지우고alice로 다시 가입할 수 있습니다. @expose, 키 하나,@login과@password.- 애플리케이션에
@signin엔티티는 하나 - 세션이 맨 키만 들고 있어, 둘이면 서로의 행으로 읽힙니다. 멀티 모듈은 컨트롤러가 생기는 모듈마다 셉니다.
계정 잠금·시도 횟수 같은 로그인 규칙은 개발자 소유 서비스에서 authenticate(login, password) 를 override 해 씁니다 -
생성된 signin 이 그것을 부릅니다. 컨트롤러는 개발자 소유라 손댄 컨트롤러에는 엔드포인트가 들어가지 않고, 빌드가
빠진 것을 알려 줍니다.
손으로 세션 다루기
가입하고 바로 로그인 같은 나만의 흐름:
SessionCurrentUser.signIn(request, customer.getCustomerId(), customer.getRole().name());
SessionCurrentUser.signOut(request);
Spring Security 를 쓰면 io.elasticore.springboot3.security.CurrentUser 빈을 등록하고 @signin 은 쓰지 않습니다
(authenticate 는 남음).
런타임 클래스
CurrentUser - id(), signedIn(), hasRole("ADMIN") | 생성된 서비스가 보는 "지금 묻는 사람" (기본 SessionCurrentUser) |
CurrentUser.runAsSystem(() -> ...) | 애플리케이션 자신: 시작 데이터, 야간 작업 |
ACCESS.require(currentUser, "report") | 직접 만든 연산에 엔티티의 @access 적용 (생성된 서비스의 필드) |
AccessPolicy, PasswordHasher | 바꿀 수 있는 빈 |
NotSignedInException (401), ForbiddenException (403) | io.elasticore.springboot3.security |
생성되는 서비스 테스트(testCode: true)는 접근 규칙이 있는 엔티티를 애플리케이션 자신으로 실행합니다
(RunAsSystemExtension). 권한 자체를 시험하는 테스트는 사용자로 로그인해서 씁니다.