회원과 관리자 권한
90분 안팎
학습 목표
인증 여부와 자원 소유권 검사를 분리합니다.
개념
회원 확인 뒤에도 거절해야 하는 이유
회원 2가 로그인해 대여 11번을 요청했지만 그 대여의 소유자는 회원 1입니다. 비밀번호 검증 성공은 회원 2의 신원만 확인합니다. 이 요청까지 허용하면 URL의 숫자만 바꿔 타인 기록을 읽을 수 있습니다. 권한은 요청 행위와 대상 자원을 함께 보고 판단합니다. 개인 대여 조회·메모 수정은 소유자에게 허용하고 도서 목록의 추가·수정·삭제는 관리자에게 허용합니다. 관리자가 모든 개인정보를 볼 수 있다는 규칙은 이 프로젝트에 없습니다.
역할과 소유권은 다른 축입니다
ADMIN은 도서 관리 업무를 나타내고 member_id는 한 대여의 소유자를 나타냅니다. 필터의 hasRole("ADMIN")은 인증 객체의 ROLE_ADMIN 권한을 검사합니다. DB role에는 ADMIN을 저장하고 User.roles가 접두어를 붙입니다. 이미 ROLE_ADMIN인 값을 roles에 넣는 식으로 형식을 섞지 않습니다. 소유권은 accounts.role만 보고 결정할 수 없습니다. 로그인 신원의 회원 ID와 loans.member_id를 비교해야 하므로 자원 조회 경계에서 검사합니다. 이 두 축을 나눠야 관리자 도서 관리와 개인 대여 보호를 동시에 표현할 수 있습니다.
권한 행렬부터 적습니다
공개 GET /books와 GET /books/{id}는 익명에게도 허용합니다. POST·PATCH·DELETE /books 계열은 관리자만 허용합니다. /me와 /me/loans는 로그인한 사용자에게 허용하고 /loans/{id}는 로그인과 소유권을 모두 검사합니다. 관리자 9도 자신이 소유하지 않은 대여 11번에는 404를 받습니다. 미션 acceptance의 관리자 성공은 도서 관리 성공을 의미합니다. 운영 규칙을 구현하기 전에 요청자별 기대 상태를 표로 적으면 서로 다른 권한을 한 조건문에 묶는 실수를 줄일 수 있습니다.
필터 규칙은 순서와 범위를 함께 봅니다
SecurityConfiguration의 authorizeHttpRequests는 /csrf와 /login, 공개 도서 GET, 관리자 도서 경로, 나머지 인증 요청 순으로 선언합니다. 도서 전체를 permitAll로 먼저 열면 뒤에 둔 관리자 검사에 도달하지 않을 수 있습니다. 변경 메서드를 포함하는 /books 경로를 마지막 관리자 규칙에 연결합니다. anyRequest().authenticated는 아직 명세에 없는 개인 경로도 기본 인증 경계 안에 둡니다. CSRF 없는 변경 요청은 이 규칙보다 앞에서 거절될 수 있으므로 역할 테스트에는 유효한 토큰을 넣어 원인을 분리합니다.
내 목록은 서버 신원으로 필터링합니다
GET /me/loans는 Principal의 name을 회원 ID로 바꾸고 WHERE l.member_id=? 조건을 붙여 조회합니다. 클라이언트가 ?memberId=2를 붙여도 신원은 바뀌지 않습니다. 화면에서 타인 목록 버튼을 숨기는 것은 이 조건의 대체 방법이 아닙니다. 목록 API가 전체 데이터를 내려주고 브라우저에서 일부만 보여 주면 이미 타인 기록은 전달된 상태입니다. 이 실습은 로그인 이름이 내부 숫자 ID라는 계약을 사용합니다. 외부 이메일이나 닉네임으로 바꿀 때는 검증된 내부 ID로 매핑하는 경계를 따로 마련합니다.
단건 조회에서 존재 정보도 보호합니다
GET /loans/11은 l.id=? AND l.member_id=?를 한 쿼리에 넣습니다. 결과가 없으면 LoanNotFound를 던지고 RESOURCE_NOT_FOUND의 404를 반환합니다. 없는 999번과 타인 11번은 같은 응답 본문을 씁니다. 먼저 ID로 조회한 후 회원 이름을 포함한 403을 만들면 타인 자원의 존재와 소유자가 드러날 수 있습니다. 404 정책은 자원 비공개를 위한 이 프로젝트의 선택이며 모든 서비스가 같은 상태를 써야 한다는 뜻은 아닙니다. 비교 테스트는 상태뿐 아니라 두 오류 본문의 동일성도 확인합니다.
수정 전에 소유권을 검사합니다
PATCH /loans/{id}/note는 소유권이 확인된 find를 먼저 호출하고 loan_notes에 메모를 저장합니다. 기존 대여 테이블을 수정해 반납을 구현하는 경로가 아닙니다. note는 null을 허용하지 않으며 빈 문자열은 메모 삭제 의미로 허용하고 길이는 UTF-16 코드 단위 200 이하입니다. 허용된 빈 문자열과 누락을 혼동하지 않습니다. 거절 이후에는 note 행 수가 0인지 SQL로 검사합니다. 예외를 던지더라도 쓰기를 먼저 했다면 타인 메모가 바뀔 수 있으므로 검사의 위치 자체가 데이터 보호와 연결됩니다.
앞 모듈의 저장 계약을 유지합니다
도서 수정과 삭제는 기존 BookService·JdbcRepository를 사용합니다. 관리자가 로그인했다고 기존 입력 검증이나 대여 이력 삭제 제한을 생략하지 않습니다. 관리자에게 허용된 삭제도 FK에 연결된 이력이 있으면 BOOK_CONFLICT의 409입니다. 권한 허용은 업무 규칙 통과와 다릅니다. 기존 members·loans 테이블의 컬럼 순서를 유지하고 계정과 메모를 별도 테이블에 추가해 앞 모듈의 SQL 준비 코드도 보존합니다. 이전 95개 테스트는 컨트롤러·저장소 계약 검사이며 보안 필터 적용 증거는 새 통합 테스트가 담당합니다.
401·403·404를 실패 원인으로 읽습니다
유효한 토큰을 가진 익명 변경 요청은 인증이 없어서 401입니다. 로그인한 MEMBER가 도서를 수정하면 역할이 부족해 403입니다. 로그인한 다른 회원의 대여 조회는 존재 정보 보호 정책에 따라 404입니다. 같은 URL에서 받은 403만 보고 관리자가 아니라고 단정하지 않습니다. 토큰 누락도 403을 만들기 때문입니다. 테스트 실패의 실제 상태와 기대 상태를 본 뒤 세션·토큰·역할·소유권 순서로 조건을 분해합니다. 거절된 DB 값이 원래 값인지까지 확인해야 요청 보호의 효과를 설명할 수 있습니다.
실습에서 고칠 두 경계
starter는 도서 관리 규칙을 authenticated로 열고 대여 SQL에서 member_id 조건을 생략했습니다. filterChain을 ADMIN 역할로 제한하고 mine·find의 쿼리에 신원 조건과 바인딩 인자를 복원합니다. note는 find를 재사용하므로 단건 조회 경계를 복구하면 타인 수정도 거절됩니다. 관리자 성공 경로도 남겨 모두 거부하는 방식으로 테스트를 맞추지 않습니다. 소유권은 이 모듈에서 고정된 대여 행을 대상으로 검사합니다. 향후 소유자 이동 기능을 만들면 검사와 쓰기 사이 변경을 트랜잭션 또는 조건부 갱신으로 보호하는 정책을 함께 설계해야 합니다.
따라하기
실패 경계와 수정 파일을 찾습니다
SecurityConfiguration의 관리자 역할 규칙과 AccessController.mine·find의 소유자 조건을 복원합니다. 로그인 요청자의 ID만 바인딩하고, 타인 메모 쓰기 전에 거절합니다. 관리자 도서 성공·회원 거절·개인 대여 비공개를 함께 검사합니다.
starter에서 먼저 실행해 실패 테스트 이름을 확인합니다. README의 권한 표와 src/test/java/lab/AccessSecurityTest.java의 해당 경로를 대조합니다.
./mvnw test > test.log 2>&1TODO를 주제에 맞게 복원합니다
아래 코드를 해당 메서드와 규칙 위치에 반영합니다. 서로 다른 메서드의 조각을 한 메서드에 붙이지 않습니다. 테스트 기대값을 바꾸는 대신 요청 경계를 수정합니다.
// filterChain: 도서 변경 경로의 역할
.requestMatchers("/books","/books/**").hasRole("ADMIN")
// mine: 로그인 신원으로 목록을 제한
return jdbc.query(SELECT+" WHERE l.member_id=? ORDER BY l.id",(r,n)->read(r),member(p));
// find: ID와 소유자를 함께 제한
return jdbc.query(SELECT+" WHERE l.id=? AND l.member_id=?",(r,n)->read(r),id,member(p))
.stream().findFirst().orElseThrow(()->new LoanNotFound());성공과 거절을 누적 검사합니다
수정한 starter 폴더에서 다시 실행합니다. 실패하면 test.log와 target/surefire-reports의 테스트 이름·기대 상태를 확인합니다. 실제 보안 테스트는 요청·응답 자동 인쇄를 꺼 비밀번호나 토큰이 로그에 남지 않도록 했습니다.
./mvnw test > test.log 2>&1실행한 보고서를 집계합니다
모범 구현에서 실행한 누적 결과입니다. 95개 기존 검사와 27개 보안 검사가 포함됩니다. 학습자의 결과도 실패·오류·건너뜀 0인지 대조합니다.
python3 report.py실행 결과
Tests=122 Failures=0 Errors=0 Skipped=0
확인 문제
실습
SecurityConfiguration의 관리자 역할 규칙과 AccessController.mine·find의 소유자 조건을 복원합니다. 로그인 요청자의 ID만 바인딩하고, 타인 메모 쓰기 전에 거절합니다. 관리자 도서 성공·회원 거절·개인 대여 비공개를 함께 검사합니다.
테스트를 삭제·비활성화하지 않고 README의 API 계약을 지킵니다. report.py의 집계와 수정 이유를 제출합니다.
실행 명령
./mvnw test
기대 결과
Tests=122, Failures=0, Errors=0, Skipped=0입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 인증에 성공했지만 수정 요청을 거부해야 하는 사례를 설명합니다.