Devin.KR

인증과 자료 접근 연결

95분 안팎

학습 목표

요청 ID와 계정·시각으로 이벤트를 연결합니다.

개념

가까운 사건을 모두 묶지 않습니다

인증 실패 다음에 자료 거절이 발생했다고 두 사건의 행위자가 같다고 말할 수는 없습니다. 여러 사용자가 동시에 앱을 쓰면 시간상 가까운 요청이 뒤섞입니다. 이번 레슨은 계정이 같고 신뢰할 수 있는 흐름 키가 일치하는 성공과 거절을 연결합니다. 실패는 계정별 마지막 실패 시각을 기준으로 묶습니다. 출력은 확인할 사건의 타임라인이며 누가 침입했다는 결론은 포함하지 않습니다.

입력과 결과를 정의합니다

브라우저 입력은 JSON 배열이며 각 원소는 id·time·account·type·requestId를 갖습니다. 사건 유형은 auth_failure·auth_success·access_denied입니다. 출력은 조건을 충족한 성공과 후속 거절의 id를 두 칸으로 표시합니다. 여러 쌍이면 성공 시각과 id, 거절 시각과 id 순으로 정렬합니다. 입력이 빈 배열이거나 연결이 없으면 아무 줄도 출력하지 않습니다. 자료 본문과 토큰은 입력 계약에 필요하지 않습니다.

흐름 키의 한계를 먼저 알립니다

교재 requestId는 서버가 발급한 한 조사 흐름의 키입니다. 일반 HTTP 요청 ID는 로그인과 다음 자료 요청에서 달라집니다. 실제 환경에서는 로그인 요청 ID와 접근 요청 ID를 억지로 같게 만들지 않고 서버 발급 별도 흐름 키 또는 제한된 내부 세션 참조로 연결합니다. 원본 세션 토큰을 로그 키로 쓰지 않습니다. 이 과제는 그 전송 설계를 구현하지 않으며 명명 계약을 이해하는 함수 연습입니다.

시각을 UTC의 순간으로 바꿉니다

datetime.fromisoformat으로 파싱하고 오프셋이 없으면 ValueError로 중단합니다. 비교 전에 astimezone으로 UTC로 맞춥니다. timezone-aware와 naive datetime을 섞으면 비교 오류가 나므로 결측 시간대에 현재 컴퓨터의 시간대를 임의로 붙이지 않습니다. 같은 순간의 사건은 id로 표시 순서를 결정할 수 있지만 그것이 실제 인과 순서를 증명하지는 않습니다. 조건은 엄격한 이후 비교를 사용합니다.

계정별 마지막 실패를 찾습니다

failures에는 계정별 실패 시각 목록을 저장합니다. 성공을 검토할 때 해당 계정의 목록이 없으면 후보가 아닙니다. 성공 시각이 max 실패 시각보다 늦어야 합니다. 따라서 성공 이후 또 실패가 있는 배열에서는 그 성공을 연결하지 않습니다. 이 정책은 전체 입력 구간을 대상으로 하므로 조사 구간을 바꾸면 결과가 바뀔 수 있습니다. 실무에서는 분석 범위를 결과와 함께 보존합니다.

성공 이후 거절을 고릅니다

성공과 거절의 계정과 requestId가 모두 같고 거절 시각이 더 늦은지 확인합니다. 같은 주소나 같은 브라우저 문자열은 이 조건을 대신하지 않습니다. 거절은 서버가 이미 소유자 정책 분기에서 기록한 사건입니다. 거절 기록은 시도와 통제 동작의 근거이지 자료 읽기 성공의 근거가 아닙니다. 외부 404와 내부 access_denied가 다른 의미임을 타임라인 해설에서도 유지합니다.

표시 순서와 의미 순서

정렬은 파일 입력 순서에 의존하지 않도록 만드는 작업입니다. 수집기 지연 때문에 늦은 사건이 먼저 도착할 수 있어 배열 순서대로 처리하면 후속 성공을 놓칠 수 있습니다. 모든 시각을 먼저 파싱하고 정렬하면 고정 fixture 결과가 안정됩니다. 서버 간 시계 오차는 정렬만으로 해결되지 않습니다. 가까운 시각의 인과 관계가 불명확하면 clock uncertainty라는 조사 한계를 기록합니다.

중복과 결측 처리의 책임

이 브라우저 과제는 필수 필드가 있는 유효한 배열을 제공하고 잘못된 오프셋은 오류로 다룹니다. 미션 분석기는 필수값 결측을 별도 missing 목록에 모읍니다. 같은 id가 반복되면 미션은 오류로 중단합니다. 실무에서는 재전송 중복인지 서로 다른 사건의 키 충돌인지 구분해야 합니다. 임의로 마지막 행을 남기면 더 유용한 원본이 사라질 수 있으므로 원본과 정규화 결과를 나눕니다.

거절 여러 건을 확인합니다

하나의 성공 뒤 여러 자료 거절이 있으면 각각의 쌍을 출력합니다. 같은 성공 id가 반복되는 것은 새로운 로그인 성공이 여러 건 있었다는 뜻이 아닙니다. 출력 행 수를 인증 성공 수로 세지 않습니다. 실패 횟수와 접근 시도 수는 다른 측정값이며 조사 보고서에서 항목명을 구체적으로 적습니다. 조회 결과의 중복처럼 보이는 표시가 정책상 여러 근거를 표현하는 경우도 있습니다.

오류 메시지에서 입력을 찾습니다

KeyError는 필수 키 이름이 없다는 신호입니다. JSONDecodeError는 줄 단위 로그를 배열인 것처럼 넣었거나 쉼표·따옴표가 깨진 경우를 확인합니다. timestamp offset missing은 시간대 없이 기록된 사건입니다. 결과가 비어 있으면 출력 함수를 고치기 전에 계정·흐름 키·엄격한 시각 비교·마지막 실패 조건을 각각 점검합니다. 조건 하나씩 작은 배열로 줄이면 원인을 찾기 쉽습니다.

타임라인을 해석합니다

f1부터 f3까지 실패 후 s1 성공과 d1 거절을 찾았다면 관찰은 그 사건들의 순서와 연결 조건입니다. 가능한 설명에는 사용자 비밀번호 오타, 공유 계정의 정상 사용, 계정 탈취 시도 등 여러 가지가 남습니다. 계정 소유자 확인, 성공 요청의 추가 메타데이터, 자료 열람 성공 기록, 수집 상태를 더 조사해야 합니다. 제한된 fixture에서 실제 공격자나 유출 여부를 단정하지 않습니다.

다른 분석과 역할을 나눕니다

집계는 어떤 계정을 먼저 볼지 줄이고 상관분석은 그 계정의 사건 관계를 드러냅니다. 두 결과가 같아 보이더라도 역할과 보장 범위가 다릅니다. 이 레슨에서는 Python 표준 라이브러리만으로 문자열 파싱과 정렬을 실행합니다. 더 읽기의 표준 라이브러리 장에서 datetime과 JSON 처리를 확장한 뒤 시간대·수집 순서·키 계약을 스스로 설명할 수 있는지 확인합니다.

결과를 동료에게 설명할 때 성공과 거절의 키가 왜 신뢰 가능한지 먼저 말합니다. 수집기가 외부 헤더 값을 그대로 복사했다면 지금 연결 결과는 그 값이 같다는 관찰에 불과합니다. 키 생성 위치를 확인하지 못했으면 관계의 신뢰도도 미확인으로 남깁니다. 일치 조건을 코드로 작성한 것과 그 필드가 실제 사용자를 식별한다는 주장은 서로 다른 근거를 요구합니다. 보고서 문장도 이 차이를 반영합니다.

따라하기

작은 조건을 직접 실행합니다

from datetime import datetime
a=datetime.fromisoformat('2026-10-09T09:00:00+09:00')
b=datetime.fromisoformat('2026-10-09T00:00:00+00:00')
print(a==b)

실행 결과

True

사건을 정렬합니다

JSON 배열을 파싱하고 timezone-aware 시각과 id로 정렬합니다.

조건을 연결합니다

계정별 마지막 실패 이후 성공을 찾고 같은 흐름 키의 더 늦은 거절을 고릅니다.

음성 대조를 봅니다

다른 계정, 역순 배열, 같은 순간의 거절 테스트에서 왜 결과가 달라지는지 설명합니다.

확인 문제

실습

필수 필드를 가진 JSON 배열입니다. 시각은 오프셋을 포함합니다. 마지막 계정 실패 이후 성공과, 그 성공 이후 같은 account·requestId의 거절을 연결해 성공 id와 거절 id를 공백으로 출력합니다. 성공 시각/id, 거절 시각/id 순으로 표시합니다. 없음은 빈 출력입니다. requestId는 교재의 서버 발급 흐름 키입니다.

모범 답안
import json,sys
from datetime import datetime,timezone
rows=json.load(sys.stdin)
def stamp(e):
 t=datetime.fromisoformat(e['time'].replace('Z','+00:00'))
 if t.tzinfo is None:raise ValueError('timestamp offset missing')
 return t.astimezone(timezone.utc)
rows=sorted(rows,key=lambda e:(stamp(e),e['id']))
failures={}
for e in rows:
 if e['type']=='auth_failure':failures.setdefault(e['account'],[]).append(stamp(e))
for s in rows:
 if s['type']!='auth_success' or not failures.get(s['account']) or stamp(s)<=max(failures[s['account']]):continue
 for d in rows:
  if d['type']=='access_denied' and d['account']==s['account'] and d['requestId']==s['requestId'] and stamp(d)>stamp(s):print(s['id'],d['id'])

더 읽기

면접 질문

  • 인증 실패 경보를 분석하는 순서를 설명합니다.