세션 로그인
90분 안팎
학습 목표
비밀번호 해시와 쿠키·세션 전달 조건을 확인합니다.
개념
왜 로그인 결과를 다음 요청까지 보관하나요
도서 목록은 공개해도 개인 대여 내역은 로그인한 회원에게만 보여 줘야 합니다. HTTP 요청마다 비밀번호를 받으면 입력·전송·로그 취급 범위가 넓어집니다. 이 프로젝트는 로그인에서 회원을 확인하고 이후에는 세션 식별자를 전달합니다. 인증은 누가 요청했는지 확인하는 절차입니다. 대여 11번을 읽어도 되는지 판단하는 인가는 다음 레슨에서 분리합니다. 로그인 성공을 모든 업무 허가로 해석하지 않습니다.
실습 환경과 누적 프로젝트
첫 실습의 starter.zip을 새 폴더에 풀고 JDK 17에서 ./mvnw test를 실행합니다. Spring Boot 3.1.5가 관리하는 Spring Security 6.1.5와 H2 2.1.214를 사용합니다. wrapper와 .mvn/wrapper가 포함되어 시스템 Maven은 필요하지 않습니다. 작성 검증은 준비된 캐시로 오프라인 실행했으며 학습자는 일반 명령을 사용합니다. 네 실습 모두 이전 모듈의 JDBC CRUD와 테스트 95개를 포함합니다. 운영 config를 복사하지 않고 MockMvc와 메모리 H2로 검증하므로 서버 포트를 열지 않습니다.
회원 원장과 로그인 계정을 나눕니다
기존 members에는 이름과 이메일이 있고 새 accounts에는 member_id·password_hash·role이 있습니다. member_id 외래키는 원장의 회원과 로그인 계정을 연결합니다. 계정이 있어도 권한은 MEMBER 또는 ADMIN으로 제한됩니다. 공개 회원가입이나 계정 관리 API는 아직 없습니다. 실행 애플리케이션에 기본 비밀번호나 기본 관리자를 넣지 않습니다. 테스트만 매번 임의의 비밀번호를 메모리에서 생성하고 PasswordEncoder.encode 결과를 DB에 넣습니다. 평문을 소스 상수·README·로그·DB에 남기지 않습니다.
복호화 대신 해시 검증을 사용합니다
BCryptPasswordEncoder는 비밀번호를 encode로 저장하고 matches로 입력과 저장 해시를 비교합니다. 저장 해시를 다시 encode한 문자열과 비교하지 않습니다. 솔트 때문에 같은 입력을 두 번 해시해도 문자열이 달라질 수 있으며 테스트는 이 차이와 올바른 입력의 matches 성공을 함께 검사합니다. 해시는 비밀번호 원문을 되찾는 암호문이 아닙니다. 그렇다고 유출해도 안전한 공개 데이터는 아닙니다. 해시가 유출되면 추측 공격 대상이 되므로 응답 DTO와 진단 로그에서 제외합니다.
폼 로그인 경로를 따라갑니다
브라우저는 GET /csrf로 현재 세션의 토큰을 받고 POST /login에 username·password 폼 파라미터와 토큰 헤더를 보냅니다. username은 이 실습의 숫자 회원 ID입니다. JSON 로그인 DTO를 보내는 경로가 아닙니다. Spring Security의 폼 로그인 필터가 UserDetailsService를 호출하고 PasswordEncoder로 검증합니다. 성공 핸들러는 204, 실패 핸들러는 LOGIN_FAILED 코드의 401을 반환합니다. 기본 HTML 로그인 페이지로 이동하는 흐름 대신 API에서 읽기 쉬운 계약을 지정한 것입니다.
저장된 세션으로 신원을 확인합니다
로그인 필터는 성공한 인증을 세션에 보관합니다. 다음 GET /me에서 세션을 전달하면 Principal.getName으로 확인된 회원 ID를 읽습니다. 요청 본문이나 memberId 파라미터에서 신원을 선택하지 않습니다. 세션을 넘기지 않은 요청은 새 익명 요청이므로 AUTH_REQUIRED와 401입니다. loginPersistsSessionAndSafeIdentity는 로그인 응답에서 얻은 MockHttpSession을 다음 요청에 사용합니다. 테스트용 사용자 주입으로 이 단계를 건너뛰면 실제 로그인 저장 누락을 발견하지 못합니다.
쿠키가 전달되는 조건을 읽습니다
실제 서블릿 컨테이너에서는 세션 ID가 보통 JSESSIONID 쿠키로 전달됩니다. 브라우저는 쿠키의 도메인·경로·만료·Secure·SameSite 조건과 요청 상황을 보고 전달 여부를 결정합니다. HttpOnly는 스크립트에서 쿠키 값을 읽는 경로를 제한하고 Secure는 HTTPS 전송 조건을 지정합니다. 어느 속성도 소유권 검사를 대신하지 않습니다. MockMvc의 session 메서드는 쿠키 네트워크 전송을 재현하지 않습니다. 이번 통과 결과로 실제 HTTPS 환경의 쿠키 속성까지 검증했다고 주장하지 않습니다.
세션 수명에서 중요한 두 순간
로그인 직전에 가지고 있던 세션 ID를 성공 후에도 그대로 믿으면 세션 고정 위험이 생깁니다. 제공된 설정에서는 로그인 성공 시 ID가 바뀌며 loginRotatesSessionId가 이를 검사합니다. POST /logout은 유효한 CSRF 토큰과 함께 보내고 성공하면 204 및 세션 무효화를 확인합니다. GET 요청으로 로그아웃시키는 API는 만들지 않습니다. 세션 만료 이후나 로그아웃 이후에는 다시 인증해야 합니다. 권한 변경 직후 이미 로그인한 세션의 권한을 언제 갱신할지는 별도 운영 정책으로 남습니다.
실패 메시지에서 확인할 순서
Expected status 204 but was 401이면 먼저 username에 대응하는 accounts 행, 해시 형식, PasswordEncoder 연결을 확인합니다. GET /me가 401이면 로그인 성공 응답에서 받은 세션을 다음 요청에도 사용했는지 확인합니다. 로그인 자체가 403이면 비밀번호보다 CSRF 토큰과 세션의 짝을 먼저 살핍니다. 계정 부재와 비밀번호 불일치는 같은 LOGIN_FAILED 응답입니다. 어떤 회원 ID가 존재하는지 실패 응답으로 구별해 주지 않습니다. 실패 원인을 조사할 때 password 파라미터나 해시를 출력하지 않습니다.
이번 TODO의 완료 기준
starter의 users 메서드는 계정 조회 결과를 신원으로 연결하지 않고 로그인 실패를 반환합니다. 매개변수 바인딩으로 accounts를 조회해 저장된 해시와 role을 가진 UserDetails를 반환하도록 완성합니다. 조회 결과가 없을 때만 UsernameNotFoundException을 던집니다. 테스트에서는 정상 로그인·잘못된 비밀번호·없는 회원·로그아웃을 함께 확인합니다. 테스트 코드의 임의 비밀번호 생성은 재현을 위한 입력이며 실제 비밀번호 운영 정책을 구현한 것은 아닙니다. 암호화 전송의 기초는 더 읽기의 DNS·TLS 장에서 이어서 확인합니다.
따라하기
실패 경계와 수정 파일을 찾습니다
SecurityConfiguration.users의 TODO를 accounts 조회로 완성합니다. 원문 저장·출력 없이 저장 해시와 역할을 UserDetails로 전달합니다. 로그인 성공·실패, 세션 ID 교체, 로그아웃 테스트를 보존합니다.
starter에서 먼저 실행해 실패 테스트 이름을 확인합니다. README의 권한 표와 src/test/java/lab/AccessSecurityTest.java의 해당 경로를 대조합니다.
./mvnw test > test.log 2>&1TODO를 주제에 맞게 복원합니다
아래 코드를 해당 메서드와 규칙 위치에 반영합니다. 서로 다른 메서드의 조각을 한 메서드에 붙이지 않습니다. 테스트 기대값을 바꾸는 대신 요청 경계를 수정합니다.
List<UserDetails> rows=accessJdbc.query(
"SELECT member_id,password_hash,role FROM accounts WHERE CAST(member_id AS VARCHAR)=?",
(r,n)->User.withUsername(r.getString(1)).password(r.getString(2)).roles(r.getString(3)).build(), username);
if(rows.isEmpty()) throw new UsernameNotFoundException("로그인 정보를 확인해 주세요");
return rows.get(0);성공과 거절을 누적 검사합니다
수정한 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.users의 TODO를 accounts 조회로 완성합니다. 원문 저장·출력 없이 저장 해시와 역할을 UserDetails로 전달합니다. 로그인 성공·실패, 세션 ID 교체, 로그아웃 테스트를 보존합니다.
테스트를 삭제·비활성화하지 않고 README의 API 계약을 지킵니다. report.py의 집계와 수정 이유를 제출합니다.
실행 명령
./mvnw test
기대 결과
Tests=122, Failures=0, Errors=0, Skipped=0입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 인증에 성공했지만 수정 요청을 거부해야 하는 사례를 설명합니다.