로그인

AUTH-LOGIN · /login

실제 화면 열기 ↗
기능
7
API 호출
1
가드레일
11
id 가 테스트 이름
구현 단계
16 / 16
완료인증 · 회원wvctesol-auth :8081클라이언트 컴포넌트197

개발 스펙

이 화면에서 할 수 있어야 하는 것 (3)

  • 아이디 · 비밀번호로 로그인한다.
  • 카카오 로그인도 쓸 수 있다.
  • 아이디/비밀번호 찾기로 이동한다.

비즈니스 규칙 (3)

  • 비밀번호는 bcrypt 로 검증한다.
    근거: src/lib/auth.ts — 이관 시 bcrypt 로 옮겨 둔 상태.
  • 수강 자격 플래그(prepare_yn · pay_yn · cyber_yn 등)를 세션에 함께 싣는다.
    근거: _Env.php 가 매 요청 계산하던 값.
  • pw 가 빈 회원이 있다(소셜 전용으로 추정). 이 계정은 비밀번호 로그인이 불가능하다.

API (3)

메서드경로권한모듈 · 설명
POST/api/identity/login공개
회원 로그인. 성공 시 액세스 토큰 발급.
domains:identity:identity-web
요청: { loginId, password }
응답: { accessToken, refreshToken, member: { id, name, entitlements } }
401 INVALID_CREDENTIALS
POST/api/identity/token/refresh공개
토큰 재발급.
domains:identity:identity-web
GET/api/identity/me수강생
내 정보와 수강 자격 플래그.
domains:identity:identity-web

가드레일 (11) — id 가 곧 테스트 이름이다

id반드시 만족해야 하는 것등급
signin-log-long-login-id
아이디가 길어도 로그인이 되어야 한다.
member_sign_log.member_id 는 varchar(20) 인데 member.login_id 는 50자까지 받는다. 자르지 않고 넣으면 INSERT 가 실패하고, REQUIRES_NEW 안에서 그 예외를 잡으면 rollback-only 로 커밋이 터져 로그인 실패로 번진다 — 실제로 그렇게 깨졌다.
critical
signin-log-recorded
로그인·로그아웃 이력이 원본 문구("로그인"/"로그아웃")로 남아야 한다.
13만 건이 2021년부터 쌓여 있다 — 값이 달라지면 한 줄로 이어 읽히지 않는다.
normal
signin-log-does-not-break-login
이력을 못 남겨도 로그인은 되어야 한다.
원본도 실패를 삼켰다. 기록 한 건보다 로그인이 되는 게 중요하다.
high
signin-log-no-record-on-failure
실패한 로그인은 이력에 남기지 않고, 통과시키지도 않아야 한다.
원본도 성공만 tMemberSignLog 에 넣었다.
high
signin-log-logout-always-ok
로그아웃은 아이디가 없거나 이상해도 500 을 내면 안 된다.
화면은 응답과 무관하게 로그아웃을 진행한다.
normal
login-success-not-reported-as-failure
로그인에 성공하면 실패 메시지가 뜨면 안 된다.
signIn 에 redirectTo 를 주면 성공 시 NEXT_REDIRECT 를 던지는데, catch 가 그것까지 삼켜 성공을 실패로 뒤집은 적이 있다.
critical
login-works-in-production-build
프로덕션 빌드에서도 로그인과 세션 조회가 되어야 한다.
Auth.js 가 Host 를 신뢰하지 않아 프로덕션에서만 UntrustedHost 로 전부 실패한 적이 있다. dev 에서는 드러나지 않는다.
critical
jwt-secret-shared-between-servers
인증 서버가 발급한 토큰을 API 서버가 검증할 수 있어야 한다.
설정이 없을 때 프로세스마다 랜덤 임시 키를 만들어, 로그인 후 모든 요청이 401 이 된 적이 있다.
critical
login-no-user-enumeration
없는 아이디와 틀린 비밀번호의 응답이 같아야 한다.
high
login-empty-password-account-blocked
pw 가 빈 계정은 어떤 비밀번호로도 로그인되면 안 된다.
bcrypt.compare 가 빈 해시에서 예외를 던지지 않도록 방어해야 한다.
critical
login-token-has-no-secret
토큰 페이로드에 비밀번호 해시 등 민감 정보를 담지 않는다.
critical

구현 계획 (16/16)

  1. SignLogRepository 포트 (SignType 이 원본 한글 문구를 들고 있다)identity-domain
  2. 별도 트랜잭션으로 기록하고 실패를 삼키는 어댑터identity-infrastructure
  3. LoginUseCase.login(ip) · logout(ip)identity-app
  4. POST /identity/logout — 토큰 방식이라 기록만 한다identity-web
  5. 세션에 loginId 를 실어 로그아웃 때 넘긴다wvctesol
  6. 도메인 모델과 포트 정의identity-domain
  7. LoginUseCase — 계정 존재 여부 은닉 · 빈 비밀번호 차단identity-app
  8. MemberJpaEntity · MemberRepository 구현 · bcrypt 인코더identity-infrastructure
  9. /api/identity/login 구현identity-web
  10. /api/identity/token/refresh 구현identity-web
  11. /api/identity/me 구현identity-web
  12. Next.js 를 BFF 로 전환 — Prisma 직접 접근 제거wvctesol
  13. BFF 라우트 /api/identity/auth/find-pw — 이게 없어 화면이 오래 망가져 있었다wvctesol
  14. BFF 라우트 /api/identity/auth/reset-password (확정)wvctesol
  15. 재설정 화면 /find-pw/reset — 메일 링크가 가리키는 곳wvctesol
  16. 재설정 토큰 키를 JwtTokenProvider 와 같은 고정 개발 키로 — 서버 둘이 나눠 실려 랜덤 키면 깨진다identity-security

서버 사이드 구현 계획 — 자바 객체별 요구사항 (11/11)

객체모듈책임 · 지켜야 할 것
Member
record
identity-domain
인증과 자격 판단에 필요한 회원 정보를 담는다.
  • 비밀번호 해시를 필드로 갖지 않는다 — 도메인 밖으로 샐 수 없게 아예 담지 않는다.
  • id 는 long. DB 가 integer 여도 도메인은 long 을 쓴다.
Entitlements
record
identity-domain
과정별 수강 자격. _Env.php:426~450 이 매 요청 계산하던 값.
  • lectureOpen(course) 와 quizOpen(course) 두 판정을 제공한다.
  • 과정마다 보는 플래그가 다르다 — TESOL/TEC/PHONICS/TEACHING 을 switch 로 갈라 섞이지 않게 한다.
  • PHONICS·TEACHING 은 시험이 없으므로 quizOpen 이 항상 false.
MemberRepository
interface
identity-domain
회원 조회 포트.
  • findPasswordHash 를 따로 둔다 — Member 에 해시를 싣지 않기 위해서다.
  • 구현을 모른다. JPA·SQL 타입이 시그니처에 나타나면 안 된다.
PasswordEncoder
interface
identity-domain
비밀번호 검증·해싱 포트.
  • needsUpgrade 를 둔다 — 관리자 계정에 레거시 MySQL PASSWORD() 해시가 섞여 있다.
LoginUseCase
service
identity-app
로그인 판단. 원본 member/login_proc.php.
  • 실패 사유를 구분하지 않는다 — 없는 아이디와 틀린 비밀번호가 같은 예외·같은 메시지여야 한다.
  • 저장된 해시가 비면 즉시 실패시킨다. bcrypt.compare 에 빈 해시를 넘기지 않는다.
  • UnauthorizedException(401) 을 던진다. DomainException(400) 이 아니다.
MemberJpaEntity
entity
identity-infrastructure
member 테이블 매핑.
  • 컬럼 타입을 실제 스키마에 정확히 맞춘다 — id 는 Integer(serial), TESOL 플래그는 bpchar, TEC·부가 과정은 varchar(1).
  • length 를 명시한다. 생략하면 varchar(255) 로 기대해 validate 가 막는다.
  • 스키마를 바꾸지 않는다. ddl-auto 는 validate 고정.
MemberRepositoryAdapter
class
identity-infrastructure
포트 구현. 레거시 표현을 도메인 타입으로 옮기는 유일한 지점.
  • Y/N 문자열과 'YYYY-MM-DD' 기간을 여기서 boolean 으로 바꾼다. 위 계층은 레거시를 모른다.
  • LegacyFormat 만 쓴다. 변환 규칙을 여기 새로 만들지 않는다.
BcryptPasswordEncoderAdapter
class
identity-infrastructure
회원 비밀번호 bcrypt 검증.
  • 해시가 비었거나 '$2' 로 시작하지 않으면 예외 대신 false 를 돌려준다.
JwtTokenProvider
class
identity-security
토큰 발급·검증.
  • 페이로드에 비밀번호·해시는 물론 수강 자격도 담지 않는다 — 자격은 바뀌므로 토큰에 굳히면 회수가 안 된다.
  • typ 클레임으로 access 와 refresh 를 구분한다. access 토큰으로 재발급받을 수 없어야 한다.
  • 만료·서명불일치·형식오류를 구분해 알리지 않는다. 모두 같은 INVALID_TOKEN 이다.
  • secret 은 환경변수로만 받는다. 없으면 임시 키로 뜨되 경고를 남긴다.
IdentityController
controller
identity-web
POST /api/identity/login.
  • 검증과 변환만 한다. 판단은 유스케이스에 있다.
  • 상태 코드를 직접 정하지 않는다 — GlobalExceptionHandler 가 예외 타입으로 결정한다.
MemberResponse
record
identity-web
회원 응답 DTO.
  • 비밀번호·해시를 어떤 경우에도 담지 않는다.
  • 수강 자격을 함께 내려 화면이 메뉴를 그릴 수 있게 한다.

테스트 방법 (11)

가드레일종류확인 방법
signin-log-long-login-ide2e20자 넘는 아이디로 가입 → 로그인·로그아웃 성공
signin-log-recordedunit로그인·로그아웃 후 "로그인:아이디" · "로그아웃:아이디" 가 순서대로 기록된다
signin-log-does-not-break-logine2e로그인이 토큰을 돌려준다
signin-log-no-record-on-failuree2e틀린 비밀번호 → INVALID_CREDENTIALS
signin-log-logout-always-oke2e아이디 없음·빈 본문에도 500 이 아니다
login-success-not-reported-as-failuree2e올바른 계정으로 로그인 → 오류 문구 없이 로그인 화면을 벗어난다
login-works-in-production-builde2enext build && next start 로 띄운 뒤 로그인 → 성공, 로그에 UntrustedHost 가 없다
jwt-secret-shared-between-serversintegrationauth(:8081) 로그인 토큰으로 api(:8080) 의 /api/learning/students/me 호출 → 200
login-no-user-enumeratione2e없는 아이디와 틀린 비밀번호 두 요청의 status·body·응답시간대가 동일
login-empty-password-account-blockedunitpw 가 빈 회원으로 임의 비밀번호 로그인 → 401, 예외 없이 처리
login-token-has-no-secrete2e발급 토큰 payload 를 디코드해 pw/hash 계열 키가 없음을 단언

스냅샷

AUTH-LOGIN 스냅샷

기능 목록 (7)

데이터
  • ·API 조회/api/identity/auth/find-id
입력
  • ·1개
  • ·제출 버튼1개
  • ·입력 필드2개
액션
  • ·서버 액션login
인증
  • ·세션 확인
  • ·로그인/로그아웃 호출

화면 이동

← 이 화면으로 오는 곳
이 화면에서 가는 곳 →

연결

API
  • /api/identity/auth/find-id
소스
src/app/login/page.tsx