553 lines
15 KiB
Markdown
553 lines
15 KiB
Markdown
# localhost:5000 로그인 화면 호출 흐름
|
|
|
|
이 문서는 브라우저에서 `http://localhost:5000/`로 접근했을 때 UserFront 로그인 화면이 뜨기까지의 순차 흐름을 정리합니다.
|
|
|
|
작성 기준:
|
|
|
|
- 로컬 URL: `http://localhost:5000/`
|
|
- gateway: `baron_gateway`
|
|
- UserFront upstream: `baron_userfront:5000`
|
|
- 로그인 화면 구현 파일: `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart`
|
|
|
|
## 1. 결론
|
|
|
|
로그인 화면의 대표 URL은 다음입니다.
|
|
|
|
- 한국어: `http://localhost:5000/ko/signin`
|
|
- 영어: `http://localhost:5000/en/signin`
|
|
|
|
로그인 화면의 실제 소스 파일 절대경로는 다음입니다.
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart
|
|
```
|
|
|
|
`http://localhost:5000/`로 접근하면 gateway가 UserFront 정적 앱을 내려주고, Flutter 앱이 기동된 뒤 `GoRouter`가 locale과 세션 상태를 판단하여 미로그인 사용자를 `/{locale}/signin`으로 보냅니다.
|
|
|
|
## 2. 전체 순차 흐름
|
|
|
|
### 2.1 브라우저 최초 요청
|
|
|
|
1. 브라우저가 다음 URL을 요청합니다.
|
|
|
|
```text
|
|
GET http://localhost:5000/
|
|
```
|
|
|
|
2. 요청은 Docker host port `5000`으로 들어오고 `baron_gateway` Nginx가 받습니다.
|
|
|
|
관련 파일:
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/gateway/nginx.conf
|
|
```
|
|
|
|
관련 설정:
|
|
|
|
```nginx
|
|
server {
|
|
listen 5000;
|
|
|
|
set $backend_upstream http://baron_backend:3000;
|
|
set $userfront_upstream http://baron_userfront:5000;
|
|
set $oathkeeper_upstream http://oathkeeper:4455;
|
|
|
|
location / {
|
|
proxy_pass $userfront_upstream;
|
|
}
|
|
}
|
|
```
|
|
|
|
3. `/`는 `location /`에 매칭되고 `baron_userfront:5000`으로 proxy됩니다.
|
|
|
|
```text
|
|
browser -> localhost:5000 -> baron_gateway -> baron_userfront:5000
|
|
```
|
|
|
|
### 2.2 UserFront 정적 bootstrap 응답
|
|
|
|
4. UserFront가 Flutter Web entry HTML을 반환합니다.
|
|
|
|
관련 파일:
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/userfront/web/index.html
|
|
```
|
|
|
|
5. `index.html`은 처음에 bootstrap shell을 보여줍니다. 이 시점에 보이는 것은 실제 Flutter 로그인 화면이 아니라 로딩용 HTML입니다.
|
|
|
|
주요 DOM:
|
|
|
|
```html
|
|
<main id="baron-bootstrap-shell" aria-live="polite">
|
|
<h1>Baron SW Portal</h1>
|
|
<div class="loader" aria-hidden="true"></div>
|
|
<p>Loading sign-in</p>
|
|
<div class="signin-preview" aria-hidden="true">Sign in</div>
|
|
</main>
|
|
```
|
|
|
|
6. `index.html`은 로컬 개발 환경에서 기존 service worker와 Flutter cache를 제거한 뒤 `flutter_bootstrap.js`를 로드합니다.
|
|
|
|
사용 함수/동작:
|
|
|
|
- `loadFlutter()`
|
|
- `navigator.serviceWorker.getRegistrations()`
|
|
- `registration.unregister()`
|
|
- `caches.keys()`
|
|
- `caches.delete()`
|
|
|
|
이후 브라우저는 일반적으로 다음 정적 리소스를 가져옵니다.
|
|
|
|
```text
|
|
GET /flutter_bootstrap.js
|
|
GET /manifest.json
|
|
GET /main.dart.mjs
|
|
GET /canvaskit/skwasm.js
|
|
GET /canvaskit/skwasm.wasm
|
|
GET /favicon.ico
|
|
```
|
|
|
|
### 2.3 Flutter 앱 시작
|
|
|
|
7. Flutter runtime이 `main.dart`의 `main()`을 실행합니다.
|
|
|
|
관련 파일:
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart
|
|
```
|
|
|
|
주요 함수:
|
|
|
|
- `main()`
|
|
- `WidgetsFlutterBinding.ensureInitialized()`
|
|
- `usePathUrlStrategy()`
|
|
- `EasyLocalization.ensureInitialized()`
|
|
- `LocaleRegistry.primeWithDefaults()`
|
|
- `LoggerService.init()`
|
|
- `runApp(...)`
|
|
|
|
8. `runApp()`은 다음 wrapper를 구성합니다.
|
|
|
|
```text
|
|
EasyLocalization
|
|
-> ProviderScope
|
|
-> BaronSSOApp
|
|
```
|
|
|
|
9. `BaronSSOApp.build()`가 `MaterialApp.router`를 생성하고 `_router`를 연결합니다.
|
|
|
|
주요 함수/객체:
|
|
|
|
- `BaronSSOApp`
|
|
- `_BaronSSOAppState.build()`
|
|
- `MaterialApp.router(...)`
|
|
- `routerConfig: _router`
|
|
|
|
## 3. GoRouter 라우팅 흐름
|
|
|
|
라우터는 `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart`에 정의되어 있습니다.
|
|
|
|
주요 객체:
|
|
|
|
```dart
|
|
final _router = GoRouter(
|
|
initialLocation: '/',
|
|
refreshListenable: AuthNotifier.instance,
|
|
routes: [...],
|
|
redirect: (context, state) { ... },
|
|
);
|
|
```
|
|
|
|
### 3.1 locale 결정
|
|
|
|
`http://localhost:5000/`에는 locale path segment가 없습니다. 따라서 앱은 선호 locale을 계산합니다.
|
|
|
|
사용 함수:
|
|
|
|
- `extractLocaleFromPath(Uri uri)`
|
|
- `resolvePreferredLocaleCode()`
|
|
- `normalizeLocaleCode(String? code)`
|
|
- `buildLocalizedPath(String localeCode, Uri uri)`
|
|
- `buildLocalizedHomePath(Uri uri, {String? preferredLocaleCode})`
|
|
|
|
관련 파일:
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart
|
|
```
|
|
|
|
locale 결정 기준:
|
|
|
|
1. 저장된 locale이 있으면 사용
|
|
2. 없으면 브라우저/디바이스 locale 사용
|
|
3. 지원하지 않는 값이면 fallback locale 사용
|
|
|
|
따라서 한국어 환경에서는 보통 `ko`, 영어 환경에서는 `en`이 됩니다.
|
|
|
|
### 3.2 세션 상태 확인
|
|
|
|
라우터와 앱 초기화 코드는 로컬 토큰 또는 쿠키 기반 세션 여부를 봅니다.
|
|
|
|
사용 함수:
|
|
|
|
- `_hasActiveLocalSession()`
|
|
- `AuthTokenStore.getToken()`
|
|
- `AuthTokenStore.usesCookie()`
|
|
- `_shouldRunStartupSessionRecovery(Uri uri)`
|
|
- `_silentSessionRecovery()`
|
|
- `AuthProxyService.getMe()`
|
|
- `AuthProxyService.getSessionStatus()`
|
|
|
|
관련 파일:
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart
|
|
```
|
|
|
|
세션 확인 시 호출될 수 있는 backend API:
|
|
|
|
```text
|
|
GET /api/v1/user/me
|
|
```
|
|
|
|
gateway 기준 흐름:
|
|
|
|
```text
|
|
browser -> /api/v1/user/me -> baron_gateway location /api -> baron_backend:3000
|
|
```
|
|
|
|
### 3.3 미로그인 상태에서 최종 로그인 route로 이동
|
|
|
|
미로그인 상태라면 최종적으로 다음 경로로 이동합니다.
|
|
|
|
```text
|
|
/{locale}/signin
|
|
```
|
|
|
|
예:
|
|
|
|
```text
|
|
http://localhost:5000/ko/signin
|
|
http://localhost:5000/en/signin
|
|
```
|
|
|
|
관련 redirect 함수:
|
|
|
|
- root route redirect: `buildLocalizedHomePath(...)`
|
|
- global router redirect: `redirect: (context, state) { ... }`
|
|
- private route guard: `_redirectPrivateLocaleRoute(...)`
|
|
- locale entry fallback: `LocaleEntryRedirectScreen._redirect()`
|
|
- signin path builder: `buildSigninRedirectPath(...)`
|
|
|
|
중요 분기:
|
|
|
|
- locale이 없으면 locale이 붙은 path로 보정합니다.
|
|
- public auth path는 그대로 통과합니다.
|
|
- 로그인하지 않았고 private path 또는 locale root에 있으면 `/{locale}/signin`으로 보냅니다.
|
|
- 로그인되어 있고 locale root에 있으면 `/{locale}/dashboard`로 보냅니다.
|
|
|
|
## 4. 로그인 화면 route와 화면 생성
|
|
|
|
로그인 화면 route는 `main.dart`의 `/:locale/signin`입니다.
|
|
|
|
브라우저 URL:
|
|
|
|
```text
|
|
http://localhost:5000/ko/signin
|
|
```
|
|
|
|
라우터 path:
|
|
|
|
```text
|
|
/:locale/signin
|
|
```
|
|
|
|
관련 코드 구조:
|
|
|
|
```dart
|
|
GoRoute(
|
|
path: 'signin',
|
|
builder: (context, state) {
|
|
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
|
final redirectUrl =
|
|
state.uri.queryParameters['redirect_uri'] ??
|
|
state.uri.queryParameters['redirect_url'];
|
|
return ScopedTheme(
|
|
controller: ThemeController.auth,
|
|
child: LoginScreen(
|
|
key: state.pageKey,
|
|
loginChallenge: loginChallenge,
|
|
redirectUrl: redirectUrl,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
```
|
|
|
|
생성되는 widget:
|
|
|
|
```text
|
|
ScopedTheme
|
|
-> LoginScreen
|
|
```
|
|
|
|
로그인 화면 구현 파일:
|
|
|
|
```text
|
|
/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart
|
|
```
|
|
|
|
주요 class/function:
|
|
|
|
- `LoginScreen`
|
|
- `_LoginScreenState`
|
|
- `_LoginScreenState.initState()`
|
|
- `_LoginScreenState.build()`
|
|
- `_handlePasswordLogin()`
|
|
- `_handleLinkLogin()`
|
|
- `_startEnchantedFlow()`
|
|
- `_pollForSession()`
|
|
- `_startQrFlow()`
|
|
- `_startQrPolling()`
|
|
- `_tryCookieSession()`
|
|
- `_attemptOidcAutoAccept()`
|
|
- `_acceptOidcLoginAndRedirect()`
|
|
- `_onLoginSuccess()`
|
|
|
|
## 5. LoginScreen 초기화 흐름
|
|
|
|
`LoginScreen`이 생성되면 `_LoginScreenState.initState()`가 실행됩니다.
|
|
|
|
초기화 순서:
|
|
|
|
1. `TabController(length: 3)` 생성
|
|
2. password/link/QR 입력 controller 준비
|
|
3. `drySend` query parameter 확인
|
|
4. `redirect_uri` 또는 `redirect_url` query parameter 확인
|
|
5. `login_challenge` 확인
|
|
6. magic link, short code, verification token이 있는지 확인
|
|
7. QR 로그인 필요 안내 notice 확인
|
|
8. OIDC login challenge가 있으면 자동 accept 시도
|
|
9. tenant-info 조회로 login ID label 커스터마이징
|
|
10. login challenge가 없으면 쿠키 세션 확인
|
|
|
|
초기 진입이 단순 `http://localhost:5000/`인 경우 보통 query parameter가 없으므로 다음 정도가 수행됩니다.
|
|
|
|
```text
|
|
LoginScreen.initState()
|
|
-> _resolveLoginChallenge()
|
|
-> _attemptOidcAutoAccept() // login_challenge 없으면 바로 return
|
|
-> AuthProxyService.getTenantInfo()
|
|
-> _tryCookieSession()
|
|
```
|
|
|
|
초기 화면 렌더링에 영향을 줄 수 있는 backend API:
|
|
|
|
```text
|
|
GET /api/v1/auth/tenant-info
|
|
GET /api/v1/user/me
|
|
```
|
|
|
|
## 6. 로그인 화면 UI 구성
|
|
|
|
`LoginScreen.build()`는 탭 기반 로그인 UI를 구성합니다.
|
|
|
|
탭:
|
|
|
|
```text
|
|
1. 비밀번호
|
|
2. 로그인 링크
|
|
3. QR 코드
|
|
```
|
|
|
|
관련 UI 코드:
|
|
|
|
- `TabBar`
|
|
- `TabBarView`
|
|
- password tab
|
|
- link tab
|
|
- QR tab
|
|
|
|
### 6.1 비밀번호 탭
|
|
|
|
UI 요소:
|
|
|
|
- 로그인 ID 입력
|
|
- 비밀번호 입력
|
|
- 로그인 버튼
|
|
|
|
주요 key:
|
|
|
|
```text
|
|
password_login_id_input
|
|
password_login_password_input
|
|
password_login_submit_button
|
|
```
|
|
|
|
사용 함수:
|
|
|
|
- `_handlePasswordLogin()`
|
|
- `AuthProxyService.loginWithPassword(...)`
|
|
- `_onLoginSuccess(...)`
|
|
|
|
호출 API:
|
|
|
|
```text
|
|
POST /api/v1/auth/password/login
|
|
```
|
|
|
|
### 6.2 로그인 링크 탭
|
|
|
|
UI 요소:
|
|
|
|
- 로그인 ID 입력
|
|
- 로그인 링크 전송 버튼
|
|
- short code 입력 UI
|
|
- 재전송 버튼
|
|
|
|
사용 함수:
|
|
|
|
- `_handleLinkLogin()`
|
|
- `_startEnchantedFlow(...)`
|
|
- `_pollForSession(...)`
|
|
- `_verifyShortCode(...)`
|
|
- `AuthProxyService.initEnchantedLink(...)`
|
|
- `AuthProxyService.pollEnchantedLink(...)`
|
|
- `AuthProxyService.verifyLoginShortCode(...)`
|
|
|
|
호출 API:
|
|
|
|
```text
|
|
POST /api/v1/auth/enchanted-link/init
|
|
POST /api/v1/auth/enchanted-link/poll
|
|
POST /api/v1/auth/login/code/verify-short
|
|
```
|
|
|
|
### 6.3 QR 코드 탭
|
|
|
|
QR 탭은 선택될 때 QR flow를 시작합니다.
|
|
|
|
사용 함수:
|
|
|
|
- `_handleTabSelection()`
|
|
- `_startQrFlow()`
|
|
- `_startQrPolling()`
|
|
- `_startCountdown()`
|
|
- `AuthProxyService.initQrLogin()`
|
|
- `AuthProxyService.pollQrStatus(...)`
|
|
|
|
호출 API:
|
|
|
|
```text
|
|
POST /api/v1/auth/qr/init
|
|
POST /api/v1/auth/qr/poll
|
|
```
|
|
|
|
## 7. OIDC login_challenge가 있는 경우
|
|
|
|
외부 RP 또는 Hydra 흐름에서 다음과 같은 URL로 들어올 수 있습니다.
|
|
|
|
```text
|
|
http://localhost:5000/ko/signin?login_challenge=...
|
|
```
|
|
|
|
이 경우 `LoginScreen`은 `login_challenge`를 보관하고 다음을 시도합니다.
|
|
|
|
1. `_attemptOidcAutoAccept()`
|
|
2. 로컬 token 또는 cookie session이 있으면 backend에 login accept 요청
|
|
3. backend가 반환한 `redirectTo`로 브라우저 이동
|
|
|
|
사용 함수:
|
|
|
|
- `_resolveLoginChallenge(...)`
|
|
- `_attemptOidcAutoAccept()`
|
|
- `_acceptOidcLoginAndRedirect(...)`
|
|
- `_redirectToOidcTarget(...)`
|
|
- `AuthProxyService.acceptOidcLogin(...)`
|
|
|
|
호출 API:
|
|
|
|
```text
|
|
POST /api/v1/auth/oidc/login/accept
|
|
```
|
|
|
|
로그인 화면에서 사용자가 비밀번호 로그인을 완료한 뒤에도 `_onLoginSuccess()`가 `login_challenge`를 감지하면 같은 accept 흐름을 수행합니다.
|
|
|
|
## 8. 페이지/route 목록
|
|
|
|
UserFront auth 관련 route는 locale 아래에 붙습니다.
|
|
|
|
| 브라우저 절대 URL 예시 | GoRouter path | 생성 화면 | 구현 파일 |
|
|
| --- | --- | --- | --- |
|
|
| `http://localhost:5000/ko/signin` | `/:locale/signin` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` |
|
|
| `http://localhost:5000/ko/login` | `/:locale/login` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` |
|
|
| `http://localhost:5000/ko/signup` | `/:locale/signup` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` |
|
|
| `http://localhost:5000/ko/registration` | `/:locale/registration` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` |
|
|
| `http://localhost:5000/ko/consent?consent_challenge=...` | `/:locale/consent` | `ConsentScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/consent_screen.dart` |
|
|
| `http://localhost:5000/ko/forgot-password` | `/:locale/forgot-password` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` |
|
|
| `http://localhost:5000/ko/recovery` | `/:locale/recovery` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` |
|
|
| `http://localhost:5000/ko/reset-password` | `/:locale/reset-password` | `ResetPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/reset_password_screen.dart` |
|
|
| `http://localhost:5000/ko/error` | `/:locale/error` | `ErrorScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/error_screen.dart` |
|
|
| `http://localhost:5000/ko/dashboard` | `/:locale/dashboard` | `DashboardScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/dashboard/presentation/dashboard_screen.dart` |
|
|
|
|
## 9. 가장 중요한 파일 요약
|
|
|
|
| 목적 | 파일 절대경로 |
|
|
| --- | --- |
|
|
| gateway proxy 설정 | `/mnt/e/h_workspace/baron-sso/gateway/nginx.conf` |
|
|
| Flutter Web HTML bootstrap | `/mnt/e/h_workspace/baron-sso/userfront/web/index.html` |
|
|
| 앱 시작점과 GoRouter 정의 | `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart` |
|
|
| 로그인 화면 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` |
|
|
| 로그인/API proxy client | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart` |
|
|
| token/cookie session store adapter | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart` |
|
|
| locale path helper | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart` |
|
|
| public auth path 판정 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/domain/login_link_route_policy.dart` |
|
|
|
|
## 10. 짧은 호출 흐름 다이어그램
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant B as Browser
|
|
participant G as baron_gateway<br/>localhost:5000
|
|
participant UF as baron_userfront:5000
|
|
participant FE as Flutter UserFront
|
|
participant BE as baron_backend:3000
|
|
|
|
B->>G: GET /
|
|
G->>UF: proxy GET /
|
|
UF-->>G: index.html
|
|
G-->>B: index.html
|
|
B->>G: GET /flutter_bootstrap.js
|
|
G->>UF: proxy static asset
|
|
B->>G: GET /main.dart.mjs 등
|
|
G->>UF: proxy static assets
|
|
FE->>FE: main()
|
|
FE->>FE: MaterialApp.router(_router)
|
|
FE->>FE: locale 결정
|
|
FE->>FE: local token/cookie 상태 확인
|
|
alt 미로그인
|
|
FE->>FE: route -> /{locale}/signin
|
|
FE->>BE: GET /api/v1/auth/tenant-info
|
|
FE->>BE: GET /api/v1/user/me (cookie/session check 가능)
|
|
FE->>B: LoginScreen 렌더링
|
|
else 로그인됨
|
|
FE->>FE: route -> /{locale}/dashboard
|
|
end
|
|
```
|
|
|
|
## 11. 디버깅 포인트
|
|
|
|
로그인 화면이 뜨지 않을 때 먼저 확인할 것:
|
|
|
|
1. `baron_gateway`가 떠 있는지
|
|
2. `baron_userfront`가 healthy인지
|
|
3. gateway 로그에 `baron_userfront could not be resolved` 또는 `connect() failed`가 있는지
|
|
4. 브라우저 Network 탭에서 `/flutter_bootstrap.js`, `/main.dart.mjs`가 200인지
|
|
5. Flutter route가 `/ko/signin` 또는 `/en/signin`으로 이동하는지
|
|
6. `/api/v1/auth/tenant-info`, `/api/v1/user/me` 실패가 화면 렌더링을 막는지
|
|
|