diff --git a/.env.sample b/.env.sample index f6f7af07..1a40fcee 100644 --- a/.env.sample +++ b/.env.sample @@ -21,6 +21,7 @@ DB_NAME=baron_sso # --- Backend Configuration --- # Must be 32 bytes. Generate with `openssl rand -hex 32` COOKIE_SECRET=super-secret-key-must-be-32-bytes! +REDIS_ADDR=redis:6379 # --- Frontend Configuration --- # Descope Project ID (Required for Auth) diff --git a/compose.infra.yaml b/compose.infra.yaml index 07622aae..3b82cd84 100644 --- a/compose.infra.yaml +++ b/compose.infra.yaml @@ -22,24 +22,26 @@ services: environment: CLICKHOUSE_USER: ${CLICKHOUSE_USER:-baron} CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-password} - ports: - - "${CLICKHOUSE_PORT_HTTP:-8123}:8123" - - "${CLICKHOUSE_PORT_NATIVE:-9000}:9000" - ulimits: - nofile: - soft: 262144 - hard: 262144 - volumes: - - clickhouse_data:/var/lib/clickhouse networks: - baron_net + + redis: + image: redis:7-alpine + container_name: baron_redis restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - baron_net + +volumes: + postgres_data: + clickhouse_data: + redis_data: networks: baron_net: name: baron_network driver: bridge - -volumes: - postgres_data: - clickhouse_data: diff --git a/dev.md b/dev.md new file mode 100644 index 00000000..82289287 --- /dev/null +++ b/dev.md @@ -0,0 +1,121 @@ +# 개발 문서: SMS 인증 기능 구현 + +## 1. 완료된 작업: SMS 인증 전체 플로우 구현 (발송 및 검증) + +### 작업 내역 + +#### Backend (`Go`) + +- [x] **Naver SENS API 연동 서비스 구현:** (`internal/service/sms_service.go`) + * **내용:** Naver SENS API를 호출하여 SMS 발송을 요청하는 핵심 모듈을 구현했습니다. + * **필요 이유:** 실제 SMS 메시지를 외부로 발송하기 위한 통신 기능이 필요했습니다. + +- [x] **API 엔드포인트 및 핸들러 추가:** (`cmd/server/main.go`, `internal/handler/auth_handler.go`) + * **내용:** `POST /api/v1/auth/sms` 엔드포인트를 추가하고, 인증 코드 생성 및 SMS 서비스 호출 로직을 구현했습니다. + * **필요 이유:** 프론트엔드에서 보낸 SMS 발송 요청을 수신하고 처리하기 위한 접점이 필요했습니다. + +- [x] **데이터 모델 정의:** (`internal/domain/sms_models.go`) + * **내용:** API 요청/응답 처리를 위한 Go 데이터 구조(struct)를 정의했습니다. + * **필요 이유:** JSON 데이터를 Go 코드 내에서 타입-세이프(type-safe)하게 다루기 위해 필요했습니다. + +- [x] **입력 값 처리 및 로깅 강화:** (`internal/handler/auth_handler.go`) + * **내용:** 전화번호에서 하이픈(`-`)을 제거하고, 발송/검증 실패 시 상세 오류를 기록하도록 구현했습니다. + * **필요 이유:** 외부 API의 요구사항을 충족시키고, 운영 중 문제 발생 시 신속하게 원인을 진단하기 위해 필요했습니다. + +- [x] **Redis 서비스 구현 및 연동:** (`internal/service/redis_service.go`, `internal/handler/auth_handler.go`) + * **내용:** Redis 클라이언트 초기화, 인증 코드 저장(`StoreVerificationCode`), 조회(`GetVerificationCode`), 삭제(`DeleteVerificationCode`) 기능을 구현하고, `AuthHandler`와 `SendSms` 함수에 Redis 저장 로직을 추가했습니다. + * **필요 이유:** 발급된 인증 코드를 임시로 저장하여 사용자가 나중에 입력한 코드와 비교 검증하기 위한 안정적인 저장소가 필요했습니다. + +- [x] **인증 코드 검증 API 엔드포인트 구현:** (`cmd/server/main.go`, `internal/handler/auth_handler.go`, `internal/domain/sms_models.go`) + * **내용:** `POST /api/v1/auth/verify-sms` 엔드포인트를 추가하고, 사용자가 입력한 코드와 Redis에 저장된 코드를 비교 검증하는 `VerifySms` 함수를 구현했습니다. 관련 데이터 모델도 추가했습니다. + * **필요 이유:** 사용자가 받은 인증 코드를 백엔드에서 확인하고, 인증 성공/실패 여부를 결정하기 위한 API와 로직이 필요했습니다. + +- [x] **인증 성공 시 JWT 발급 로직:** (`internal/handler/auth_handler.go`) + * **내용:** 코드 검증이 성공하면, 해당 사용자에 대한 세션 토큰(JWT)을 생성하여 프론트엔드에 반환합니다. + * **필요 이유:** 사용자가 로그인 상태를 유지하고, 인증이 필요한 다른 API를 호출할 수 있도록 하기 위함입니다. + +#### Frontend (`Flutter`) + +- [x] **SMS 인증 요청 서비스 구현:** (`lib/core/services/auth_proxy_service.dart`) + * **내용:** 백엔드의 SMS 발송 API를 호출하는 `sendSms` 함수를 추가했습니다. + * **필요 이유:** UI와 백엔드 API 간의 통신을 담당하는 재사용 가능한 서비스 계층을 만들어 코드의 관심사를 분리하기 위해 필요했습니다. + +- [x] **로그인 화면 UI/UX 개선:** (`lib/features/auth/presentation/login_screen.dart`) + * **내용:** 이메일/SMS 인증 탭 UI와 전화번호 입력 필드 및 버튼을 구현했습니다. + * **필요 이유:** 사용자가 SMS 인증 방식을 선택하고 사용할 수 있는 명확한 UI가 필요했습니다. + +- [x] **SMS 인증 발송 플로우 로직 구현:** (`lib/features/auth/presentation/login_screen.dart`) + * **내용:** '인증 코드 발송' 버튼 클릭 시 API를 호출하고, 결과에 따라 UI 상태를 동적으로 변경하는 로직을 구현했습니다. + * **필요 이유:** 사용자의 상호작용에 응답하고, 통신 결과에 따라 UI를 동적으로 변경하는 로직이 필요했습니다. + +- [x] **인증 코드 검증 서비스 구현:** (`lib/core/services/auth_proxy_service.dart`) + * **내용:** `AuthProxyService`에 백엔드의 `verify-sms` API를 호출하는 함수를 추가했습니다. + * **필요 이유:** 인증 코드 검증을 위해 백엔드와 통신하는 서비스 로직이 필요했습니다. + +- [x] **인증 코드 검증 플로우 로직 구현:** (`lib/features/auth/presentation/login_screen.dart`) + * **내용:** '코드 확인' 버튼 클릭 시, 입력된 코드와 전화번호를 백엔드로 보내 검증을 요청하는 로직을 구현했습니다. + * **필요 이유:** 사용자의 코드 제출 액션에 응답하는 로직이 필요했습니다. + +- [x] **인증 결과 처리:** (`lib/features/auth/presentation/login_screen.dart`) + * **내용:** 검증 성공 시 JWT를 콘솔에 출력하고 성공 다이얼로그를 표시합니다. (JWT 저장 및 대시보드 이동은 `TODO` 상태) + * **필요 이유:** 인증 결과에 따라 사용자에게 적절한 피드백(화면 전환 또는 오류 안내)을 제공해야 합니다. + +### 전체 인증 플로우: 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + participant 사용자 + participant Flutter as Flutter (Frontend) + participant Go as Go (Backend) + participant NaverSMS as Naver SMS API + + Note over 사용자, NaverSMS: 1. 인증 코드 발송 단계 + 사용자->>Flutter: 전화번호 입력 후 '인증 코드 발송' 요청 + Flutter->>Go: POST /api/v1/auth/sms + Go->>Go: 6자리 인증 코드 생성 & 임시 저장 (Redis) + Go->>NaverSMS: SMS 발송 요청 (인증 코드 포함) + NaverSMS-->>사용자: [SMS] 인증번호 메시지 수신 + NaverSMS-->>Go: 발송 요청 결과 응답 + Go-->>Flutter: 처리 결과 응답 (성공) + Flutter->>사용자: UI 업데이트 (인증번호 입력창 표시) + + Note over 사용자, Go: 2. 인증 코드 검증 단계 + 사용자->>Flutter: 수신한 인증 코드 입력 후 '코드 확인' 요청 + Flutter->>Go: POST /api/v1/auth/verify-sms (전화번호, 입력 코드) + Go->>Go: 임시 저장된 코드와 비교 및 검증 (Redis) + + alt 인증 성공 + Go->>Go: JWT(세션 토큰) 생성 + Go-->>Flutter: 인증 성공 응답 (JWT 포함) + Flutter->>Flutter: JWT 수신 및 처리 (저장 및 대시보드 이동 예정) + Flutter->>사용자: 성공 메시지 표시 + else 인증 실패 + Go-->>Flutter: 인증 실패 응답 + Flutter->>사용자: 오류 메시지 표시 + end + +``` + +--- + +## 2. 발생 이슈 및 해결 과정 (통합) + +1. **발신자 번호 미인증 오류** + * **이슈:** SENS API에서 `'from' is not an authenticated tel number` 오류 반환. + * **해결:** 네이버 클라우드 플랫폼 콘솔에서 SMS 발신 번호를 등록하고 인증 절차를 완료한 후, `.env` 파일의 `NAVER_SENDER_PHONE_NUMBER` 값을 인증된 번호로 수정. + +2. **환경 변수 누락으로 인한 API URL 생성 실패** + * **이슈:** 백엔드 컨테이너가 `NAVER_CLOUD_SERVICE_ID` 환경 변수를 읽지 못해 `URL not found` 오류 발생. + * **해결:** `docker-compose.yaml`의 `backend` 서비스에 `env_file: [".env"]` 설정을 추가하여 컨테이너가 `.env` 파일의 모든 변수를 로드하도록 수정. + +3. **전화번호 형식 불일치 오류** + * **이슈:** SENS API에서 `phone number format excluding dash(-)` 오류 반환. + * **해결:** 백엔드 `AuthHandler`에서 SENS API로 전화번호를 전달하기 전, `strings.ReplaceAll` 함수를 사용하여 하이픈(-)을 모두 제거하는 로직 추가. + +4. **Redis 연결 실패 오류** + * **이슈:** 백엔드 컨테이너가 Redis에 접속하지 못하고 `Failed to connect to Redis: dial tcp [::1]:6379: connect: connection refused` 오류 발생. + * **해결:** 프로젝트 루트의 `.env` 파일에 `REDIS_ADDR=redis:6379`를 추가하고, `docker-compose up -d --force-recreate` 명령을 통해 컨테이너를 강제로 재생성하여 변경된 환경 변수를 적용. + +5. **Go 백엔드 빌드 실패 (JWT 라이브러리 import 누락)** + * **이슈:** Docker 이미지 빌드 중 `undefined: jwt` 컴파일 오류 발생. + * **해결:** `backend/internal/handler/auth_handler.go` 파일에 `"github.com/golang-jwt/jwt/v4"` import 구문을 추가하여 해결. \ No newline at end of file diff --git a/main-dev.md b/main-dev.md new file mode 100644 index 00000000..2755fffe --- /dev/null +++ b/main-dev.md @@ -0,0 +1,83 @@ +# 개발 문서: 메인 프로젝트 Baron SSO 연동 작업 + +## 1. 연동 목표 + +Baron SSO(`http://localhost:5000`)를 통합 인증(SSO) 공급자로 사용하여 메인 프로젝트의 사용자 인증을 처리합니다. 사용자가 Baron SSO를 통해 성공적으로 인증하면, 메인 프로젝트는 JWT를 수신하여 로그인 상태를 유지하고, 보호된 백엔드 API에 접근할 수 있게 됩니다. + +--- + +## 2. 작업 계획 (체크리스트) + +### Frontend + +- [ ] **Baron SSO 팝업창 실행 로직 구현:** + * **내용:** '로그인' 버튼 클릭 시, `window.open()`을 사용하여 Baron SSO 로그인 페이지(`http://localhost:5000`)를 팝업창으로 엽니다. + * **필요 이유:** 사용자에게 익숙한 팝업창을 통해 소셜 로그인과 유사한 인증 경험을 제공합니다. + +- [ ] **JWT 수신을 위한 이벤트 리스너 추가:** + * **내용:** Baron SSO 팝업창으로부터 JWT 토큰을 수신하기 위해 `window.addEventListener('message', ...)`를 구현합니다. Baron SSO는 인증 성공 시 `postMessage`를 통해 토큰을 전달할 것입니다. + * **필요 이유:** 부모 창(메인 프로젝트)과 자식 창(Baron SSO) 간의 안전한 통신 채널을 확보하여 인증 결과(JWT)를 전달받기 위함입니다. + +- [ ] **인증 성공 후 처리 로직 구현:** + * **내용:** 이벤트 리스너가 JWT를 성공적으로 수신하면 다음을 수행합니다. + 1. 수신한 JWT를 `localStorage` 또는 `sessionStorage`에 안전하게 저장합니다. + 2. 열려있는 Baron SSO 팝업창을 닫습니다. (`popup.close()`) + 3. 애플리케이션의 상태를 '로그인 완료'로 변경하고, 사용자를 대시보드 등 로그인 후 페이지로 리디렉션합니다. + * **필요 이유:** 사용자의 로그인 상태를 애플리케이션 전반에 걸쳐 유지하고, 원활한 사용자 경험을 제공하기 위함입니다. + +- [ ] **API 요청 시 JWT 헤더 추가:** + * **내용:** 메인 프로젝트의 백엔드 API를 호출하는 모든 요청의 `Authorization` 헤더에 저장된 JWT를 `Bearer ` 형식으로 포함하도록 API 클라이언트(e.g., axios, fetch)를 수정합니다. + * **필요 이유:** 백엔드에 현재 사용자가 인증되었음을 증명하고, 보호된 리소스에 접근 권한을 얻기 위함입니다. + +### Backend + +- [ ] **JWT 검증 미들웨어 구현:** + * **내용:** API 요청의 `Authorization` 헤더에서 JWT를 추출하고, 유효성을 검증하는 미들웨어를 작성합니다. 검증 과정은 다음을 포함해야 합니다. + 1. 토큰의 서명을 Baron SSO와 공유하는 `COOKIE_SECRET`을 사용하여 확인합니다. + 2. 토큰의 만료 시간(`exp`)을 확인합니다. + * **필요 이유:** 인증되지 않은 사용자가 보호된 API 엔드포인트에 접근하는 것을 막는 핵심 보안 계층입니다. + +- [ ] **보호된 라우트에 미들웨어 적용:** + * **내용:** 사용자 정보가 필요하거나 인증된 사용자만 접근해야 하는 모든 API 라우트(e.g., `/api/me`, `/api/posts`)에 위에서 구현한 JWT 검증 미들웨어를 적용합니다. + * **필요 이유:** 특정 API들을 선택적으로 보호하여 서비스의 보안 수준을 높입니다. + +- [ ] **(선택) 사용자 정보 컨텍스트 주입:** + * **내용:** 미들웨어에서 토큰 검증이 성공하면, 토큰의 `claims`(e.g., `sub`에 저장된 전화번호)에서 사용자 식별 정보를 추출하여 해당 요청의 컨텍스트(context)에 담아줍니다. + * **필요 이유:** 각 API 핸들러가 현재 요청을 보낸 사용자가 누구인지 쉽게 파악하고, 해당 사용자에 맞는 비즈니스 로직을 처리할 수 있도록 하기 위함입니다. + +--- + +## 3. 연동 후 인증 플로우: 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + participant 사용자 + participant MainAppFE as 메인 프로젝트 (FE) + participant BaronSSO as Baron SSO (팝업) + participant MainAppBE as 메인 프로젝트 (BE) + + Note over 사용자, BaronSSO: 1. 로그인 요청 및 인증 + 사용자->>MainAppFE: 로그인 버튼 클릭 + MainAppFE->>BaronSSO: 팝업창으로 SSO 페이지 실행 + BaronSSO-->>사용자: SMS 인증 등 수행 + BaronSSO->>BaronSSO: 인증 성공 후 JWT 생성 + BaronSSO-->>MainAppFE: `postMessage`로 JWT 전달 + + Note over MainAppFE, MainAppBE: 2. 로그인 처리 및 API 접근 + MainAppFE->>MainAppFE: JWT 수신 및 저장 + MainAppFE->>MainAppFE: 팝업창 닫기 & 대시보드 이동 + + 사용자->>MainAppFE: 데이터 요청 (e.g., 내 정보 보기) + MainAppFE->>MainAppBE: API 요청 (Authorization: Bearer JWT 헤더 포함) + + MainAppBE->>MainAppBE: JWT 검증 미들웨어 실행 + alt JWT 유효 + MainAppBE->>MainAppBE: API 로직 처리 + MainAppBE-->>MainAppFE: 요청한 데이터 응답 (e.g., 사용자 정보) + else JWT 유효하지 않음 + MainAppBE-->>MainAppFE: 401 Unauthorized 에러 응답 + end + + MainAppFE->>사용자: 수신한 데이터 표시 또는 에러 처리 + +```