권한 회귀와 개인정보
90분 안팎
학습 목표
권한 행렬과 민감 정보 처리 범위를 테스트로 고정합니다.
개념
성공 경로만으로 권한을 증명할 수 없습니다
관리자 수정 성공을 확인해도 일반 회원의 수정이 함께 허용되는 오류는 남을 수 있습니다. 반대로 모든 요청을 거부하면 타인 수정 금지는 만족하지만 API는 쓸 수 없습니다. 권한 회귀는 허용과 거절을 같은 행렬에서 확인합니다. 이번 레슨은 익명·회원 1·회원 2·관리자 9와 공개 조회·내 목록·단건 대여·도서 변경·메모 변경의 조합을 다룹니다. 수료 증거에는 테스트 이름·기대 상태·DB 관찰을 연결해 적습니다. 구현 클래스 수나 로그인 화면 유무를 권한 완성의 증거로 삼지 않습니다.
필터가 있는 검증과 없는 검증을 구별합니다
앞 단계 ApiContractTest·JdbcApiTest는 standaloneSetup으로 컨트롤러와 저장소 계약을 검사합니다. 이 검사는 JSON 입력 오류나 JDBC 반영을 확인하지만 Spring Security 필터를 연결하지 않으므로 로그인·CSRF의 증거가 아닙니다. 새 AccessSecurityTest는 @SpringBootTest와 @AutoConfigureMockMvc로 애플리케이션 빈 및 보안 필터를 사용합니다. 실제 POST /login으로 세션을 얻고 /csrf로 토큰을 가져옵니다. 이전 검사를 지우지 않고 서로 다른 검증 범위를 문서에 적어 누적 프로젝트의 회귀를 보존합니다.
요청자를 독립된 세션으로 준비합니다
각 테스트는 H2의 loan_notes·loans·accounts·members·books를 의존성 역순으로 비우고 다시 준비합니다. 회원들의 비밀번호 원문은 실행 중 임의 생성한 메모리 값이고 DB에는 솔트가 있는 해시만 저장합니다. login(1)과 login(2)가 서로 다른 세션을 만들므로 신원이 섞이지 않습니다. 정적 세션을 공유하거나 한 테스트의 관리자 로그인을 다음 테스트에서 재사용하면 요청자의 역할이 우연히 바뀔 수 있습니다. 테스트 실행 순서를 고정해 이 문제를 숨기지 않고 각 검사의 준비가 독립적이게 유지합니다.
행렬을 관찰 가능한 기대값으로 바꿉니다
익명의 GET /me는 401이며 공개 도서 조회는 200입니다. 회원 1의 /me/loans는 11번만, 회원 2는 22번만 포함합니다. 회원 2와 관리자 9가 대여 11번을 읽으면 404입니다. 유효한 토큰이 있는 일반 회원의 도서 PATCH·DELETE·POST는 403이고 관리자 PATCH·POST는 성공합니다. 관리자 DELETE는 이력이 없는 8번에서 204이고 이력이 있는 7번에서는 409입니다. 기대값을 행위별로 써 두면 모두 관리자 허용 같은 모호한 문장이 기존 업무 제약까지 지워 버리는 문제를 피할 수 있습니다.
개인정보는 응답을 만드는 경계에서 줄입니다
내 신원 응답은 memberId 한 필드이고 대여 DTO는 id·bookId·returned·note만 포함합니다. accounts.password_hash, members.email, 타인 member_id를 응답에 직렬화하지 않습니다. 엔티티나 UserDetails 전체를 반환한 뒤 화면에서 숨기는 방식은 네트워크 응답의 노출을 줄이지 않습니다. 꼭 필요한 식별자와 업무 값만 가진 DTO로 투영합니다. note는 사용자가 입력하는 개인 메모라서 소유권 경계를 통과한 뒤에만 내려줍니다. 관리자 업무에도 개인 메모 열람권을 자동으로 붙이지 않습니다.
금지 문자열 검사와 필드 허용 목록
응답에 password라는 단어가 없다는 검사만으로 안전을 보장하기 어렵습니다. 해시 필드를 credential이라고 이름 바꾸면 금지 문자열 검사를 지나갈 수 있습니다. loginPersistsSessionAndSafeIdentity와 ownerDetailHasOnlyApprovedFields는 content().json의 strict=true로 필드 허용 목록을 고정합니다. 이름이 무엇이든 승인하지 않은 추가 필드가 들어오면 실패합니다. 타인·없는 대여의 404 본문도 비교해 존재 정보가 새 메시지로 드러나는 회귀를 잡습니다. 필요한 새 필드는 리뷰에서 공개 범위를 판단한 뒤 기대 DTO와 테스트를 함께 바꿉니다.
거절을 저장 결과와 연결합니다
memberCannotPatchBookEvenWithValidToken은 403 뒤 SQL로 제목이 보존됐는지 읽습니다. otherCannotChangeNoteAndDatabaseIsUnchanged는 404 뒤 loan_notes 행이 0개인지 검사합니다. 유효 토큰과 로그인 조건을 갖춘 상태에서 거절을 확인하므로 CSRF 실패가 소유권 오류를 가려 주지 않습니다. invalidCsrfIs403WithoutWrite는 관리자를 사용해 역할 성공 조건 아래 토큰 실패만 확인합니다. 한 요청에서 여러 실패 조건을 동시에 넣으면 가장 먼저 거절하는 경계만 검증되므로 원인별로 입력을 나눠 준비합니다.
경계 입력과 성공 후 조회를 남깁니다
메모의 {}·null·201 코드 단위는 400이고 DB 변경은 없습니다. 빈 메모와 정확히 200 코드 단위는 성공합니다. ownerCanChangeNoteWithRealToken은 응답 note와 SQL 저장값을 함께 검사합니다. 삭제 성공 뒤에는 책 8번의 DB 행 수가 0인지 확인하며 대여 이력 삭제 거절 뒤에는 loans 두 행이 남아 있는지 확인합니다. API 상태만 검사하는 것보다 업무 사실까지 관찰하는 검사가 회귀의 의미를 설명하기 쉽습니다. H2 결과가 운영 DB의 모든 문법이나 잠금 동작을 검증하지는 않으므로 이 범위도 결과 문서에 남깁니다.
실패 보고서의 내용을 읽습니다
Expected status 404 but was 200은 존재 정보 보호 또는 소유권 조건이 빠진 신호입니다. JSON 비교에서 Unexpected: passwordHash가 보이면 신원 응답에 승인하지 않은 필드가 생긴 것입니다. Failures는 기대 결과 불일치이고 Errors는 컴파일 이후 실행 환경·예외 등으로 검사 자체가 정상 진행되지 않은 경우를 먼저 조사합니다. report.py는 Surefire XML을 합산해 Tests·Failures·Errors·Skipped를 출력합니다. 실패를 숨기려고 @Disabled를 넣거나 테스트를 지우면 수료 조건의 검사 수와 건너뜀 기준을 충족하지 못합니다.
실습과 미션의 제출물을 완성합니다
starter의 /me는 테스트용 passwordHash 필드를 추가한 잘못된 응답을 반환합니다. Me DTO만 반환하도록 복구하고 엄격 JSON 비교 테스트를 유지합니다. 이번 실습은 응답 경계를 직접 고쳐 검사의 효과를 확인하는 과제이며 통과하도록 테스트 기대값을 넓히는 과제가 아닙니다. 미션에서는 앞의 로그인 조회·역할·소유권·CSRF 수정도 함께 완성합니다. README에 권한 표와 필터·컨트롤러 검사 범위를 적고 민감 정보를 포함하지 않은 보고서만 제출합니다. 더 읽기의 JUnit 장에서는 의존성 분리와 테스트 비용을 이어서 살펴봅니다.
따라하기
실패 경계와 수정 파일을 찾습니다
AccessController.me의 추가 passwordHash 응답을 제거하고 Me DTO만 반환합니다. 허용 필드 검사를 느슨하게 바꾸지 않습니다. 권한 행렬과 DB 불변 검사를 읽고 실패 원인을 신원·토큰·역할·소유권으로 구분한 제출 기록을 만듭니다.
starter에서 먼저 실행해 실패 테스트 이름을 확인합니다. README의 권한 표와 src/test/java/lab/AccessSecurityTest.java의 해당 경로를 대조합니다.
./mvnw test > test.log 2>&1TODO를 주제에 맞게 복원합니다
아래 코드를 해당 메서드와 규칙 위치에 반영합니다. 서로 다른 메서드의 조각을 한 메서드에 붙이지 않습니다. 테스트 기대값을 바꾸는 대신 요청 경계를 수정합니다.
@GetMapping("/me") public Me me(Principal p) {
return new Me(p.getName());
}
// 기존 엄격 비교 테스트를 유지합니다.
mvc.perform(get("/me").session(session))
.andExpect(status().isOk())
.andExpect(content().json("{\"memberId\":\"1\"}",true));성공과 거절을 누적 검사합니다
수정한 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
확인 문제
실습
AccessController.me의 추가 passwordHash 응답을 제거하고 Me DTO만 반환합니다. 허용 필드 검사를 느슨하게 바꾸지 않습니다. 권한 행렬과 DB 불변 검사를 읽고 실패 원인을 신원·토큰·역할·소유권으로 구분한 제출 기록을 만듭니다.
테스트를 삭제·비활성화하지 않고 README의 API 계약을 지킵니다. report.py의 집계와 수정 이유를 제출합니다.
실행 명령
./mvnw test
기대 결과
Tests=122, Failures=0, Errors=0, Skipped=0입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 인증에 성공했지만 수정 요청을 거부해야 하는 사례를 설명합니다.