플러터:인증(OAuth2)

학교의 모든 지식. SMwiki
둘러보기로 이동 검색으로 이동

틀:플러터 Dart:개요 플러터에 대한 지식 분류

  1. 플러터:개요
    1. 플러터:VSCode
  2. 플러터:실행
  3. 개념 잡기
    1. 플러터:화면 하나 만들기
    2. 플러터:변하는 화면(StatefulWidget)
    3. 플러터:화면 전환(화면 쌓기, 하단 네비게이션 바)
    4. 플러터:화면 전환(Drawer)
    5. 플러터:키보드 입력
    6. 플러터:슬라이더
    7. 플러터:그래프 그리기(fl chart)
    8. 플러터:데이터 저장(간단한 데이터)
    9. 플러터:인증(Firebase 인증)
    10. 플러터:인증(OAuth2)
  4. 권한 사용
    1. 플러터:마이크 입력
  5. 위젯
    1. 플러터:아이콘
    2. 플러터:레이아웃 계열 위젯
    3. 플러터:네비게이션 계열 위젯
    4. 플러터:버튼
    5. 플러터:상태관리
  6. 플러터:DB연결
    1. 플러터:Firebase
    2. 플러터:MySQL
  7. 디자인
    1. 플러터:테마
    2. 플러터:앱바
    3. 플러터:버튼
  8. 플러터:배포
  9. 플러터:참고자료
  10. 플러터:위젯
    1. 플러터:공간배치용 위젯
  11. 플러터:라이브러리
    1. 플러터:logger

커스텀 OAuth2 인증 (네이버, 카카오 등)[편집 | 원본 편집]

Firebase는 Google/Apple/GitHub 등 주요 소셜 로그인만 공식 지원합니다. 네이버, 카카오, 디스코드 등의 OAuth2 인증은 직접 구현해야 합니다.

방식 비교[편집 | 원본 편집]

방식 장점 단점 사용 케이스
Firebase Auth 공식 설정만으로 완성, 자동 토큰 관리 지원 제공업체만 가능 Google, Apple, GitHub, Microsoft
Firebase + Custom Token Firebase 생태계 유지 백엔드 서버 필요 네이버/카카오 + Firestore 함께 사용
순수 OAuth2 직접 구현 완전한 제어, 백엔드 불필요 토큰/세션 직접 관리 간단한 앱, 학습용

네이버 OAuth2 예시 (순수 구현)[편집 | 원본 편집]

사전 준비[편집 | 원본 편집]

단계 설명
네이버 개발자센터 설정
  1. https://developers.naver.com/apps 접속
  2. 애플리케이션 등록
  3. Client ID, Client Secret 발급
  4. Callback URL 설정: http://localhost/callback (웹) 또는 커스텀 스킴
패키지 설치
dependencies:
  http: ^1.1.0              # HTTP 요청용
  flutter_secure_storage: ^9.0.0  # 토큰 안전 저장
  url_launcher: ^6.2.1      # OAuth 웹뷰 열기

OAuth2 흐름[편집 | 원본 편집]

  1. 사용자가 "네이버 로그인" 버튼 클릭
  2. 네이버 로그인 페이지로 이동 (브라우저/웹뷰)
  3. 사용자 로그인 → 네이버가 Authorization Code 발급
  4. 앱이 Code를 받아서 Access Token 요청
  5. Access Token으로 사용자 정보 API 호출

구현 코드[편집 | 원본 편집]

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class NaverAuthService {
  // 네이버 개발자센터에서 발급받은 정보
  static const String clientId = 'YOUR_CLIENT_ID';
  static const String clientSecret = 'YOUR_CLIENT_SECRET';
  static const String redirectUri = 'http://localhost/callback';
  
  final storage = const FlutterSecureStorage();

  // 1단계: 네이버 로그인 페이지 열기
  Future<void> signIn() async {
    final authUrl = Uri.parse(
      'https://nid.naver.com/oauth2.0/authorize'
      '?response_type=code'
      '&client_id=$clientId'
      '&redirect_uri=$redirectUri'
      '&state=RANDOM_STATE', // CSRF 방지용 랜덤 문자열
    );

    if (await canLaunchUrl(authUrl)) {
      await launchUrl(authUrl, mode: LaunchMode.externalApplication);
      
      // 실제로는 Callback을 받아야 함 (아래 참고)
      // 웹: window.location 감지
      // 모바일: Deep Link 설정 필요
    }
  }

  // 2단계: Authorization Code로 Access Token 받기
  Future<String?> getAccessToken(String code) async {
    final response = await http.post(
      Uri.parse('https://nid.naver.com/oauth2.0/token'),
      body: {
        'grant_type': 'authorization_code',
        'client_id': clientId,
        'client_secret': clientSecret,
        'code': code,
        'state': 'RANDOM_STATE',
      },
    );

    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      final accessToken = data['access_token'];
      
      // 안전하게 토큰 저장
      await storage.write(key: 'naver_token', value: accessToken);
      return accessToken;
    }
    return null;
  }

  // 3단계: Access Token으로 사용자 정보 가져오기
  Future<Map<String, dynamic>?> getUserInfo() async {
    final token = await storage.read(key: 'naver_token');
    if (token == null) return null;

    final response = await http.get(
      Uri.parse('https://openapi.naver.com/v1/nid/me'),
      headers: {'Authorization': 'Bearer $token'},
    );

    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return data['response']; // {id, name, email, profile_image, ...}
    }
    return null;
  }

  // 로그아웃
  Future<void> signOut() async {
    await storage.delete(key: 'naver_token');
  }
}

// UI 예시
class NaverLoginPage extends StatefulWidget {
  const NaverLoginPage({super.key});

  @override
  State<NaverLoginPage> createState() => _NaverLoginPageState();
}

class _NaverLoginPageState extends State<NaverLoginPage> {
  final NaverAuthService _authService = NaverAuthService();
  Map<String, dynamic>? _userInfo;

  @override
  void initState() {
    super.initState();
    _checkLoginStatus();
  }

  Future<void> _checkLoginStatus() async {
    final userInfo = await _authService.getUserInfo();
    if (userInfo != null) {
      setState(() => _userInfo = userInfo);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('네이버 로그인')),
      body: Center(
        child: _userInfo == null
            ? ElevatedButton(
                onPressed: () => _authService.signIn(),
                child: const Text('네이버 로그인'),
              )
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('이름: ${_userInfo!['name']}'),
                  Text('이메일: ${_userInfo!['email']}'),
                  ElevatedButton(
                    onPressed: () async {
                      await _authService.signOut();
                      setState(() => _userInfo = null);
                    },
                    child: const Text('로그아웃'),
                  ),
                ],
              ),
      ),
    );
  }
}

Callback 처리 (중요!)[편집 | 원본 편집]

[편집 | 원본 편집]

import 'dart:html' as html;

void main() {
  // URL에서 code 파라미터 추출
  final uri = Uri.parse(html.window.location.href);
  final code = uri.queryParameters['code'];
  
  if (code != null) {
    // Access Token 받기
    NaverAuthService().getAccessToken(code);
  }
  
  runApp(const MyApp());
}

Android (Deep Link)[편집 | 원본 편집]

android/app/src/main/AndroidManifest.xml:

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="yourapp"
        android:host="callback" />
</intent-filter>

Flutter에서 받기:

import 'package:uni_links/uni_links.dart';

void initState() {
  super.initState();
  
  // Deep Link 리스너
  uriLinkStream.listen((Uri? uri) {
    if (uri != null) {
      final code = uri.queryParameters['code'];
      if (code != null) {
        _authService.getAccessToken(code);
      }
    }
  });
}

Firebase Custom Token 방식[편집 | 원본 편집]

백엔드 서버가 있다면 더 안전한 방식:

흐름[편집 | 원본 편집]

  1. Flutter → 네이버 OAuth로 Access Token 받기
  2. Flutter → 백엔드 서버로 Access Token 전송
  3. 백엔드 → 네이버 API로 사용자 정보 확인
  4. 백엔드 → Firebase Admin SDK로 Custom Token 생성
  5. 백엔드 → Flutter로 Custom Token 전송
  6. Flutter → Firebase.signInWithCustomToken()으로 로그인

장점[편집 | 원본 편집]

  • Client Secret이 앱에 노출 안 됨 (보안 ↑)
  • Firebase Authentication + Firestore 연동 가능
  • Firebase Security Rules 사용 가능

백엔드 예시 (Node.js)[편집 | 원본 편집]

const admin = require('firebase-admin');

app.post('/auth/naver', async (req, res) => {
  const { accessToken } = req.body;
  
  // 1. 네이버 사용자 정보 확인
  const naverUser = await fetch('https://openapi.naver.com/v1/nid/me', {
    headers: { 'Authorization': `Bearer ${accessToken}` }
  }).then(r => r.json());
  
  // 2. Firebase Custom Token 생성
  const customToken = await admin.auth().createCustomToken(naverUser.response.id);
  
  res.json({ customToken });
});

Flutter에서 사용[편집 | 원본 편집]

// 1. 네이버 로그인 → Access Token 받기
final accessToken = await getNaverAccessToken();

// 2. 백엔드로 전송 → Custom Token 받기
final response = await http.post(
  Uri.parse('https://yourserver.com/auth/naver'),
  body: {'accessToken': accessToken},
);
final customToken = json.decode(response.body)['customToken'];

// 3. Firebase 로그인
await FirebaseAuth.instance.signInWithCustomToken(customToken);

// 4. 이제 Firebase 기능 모두 사용 가능!
final user = FirebaseAuth.instance.currentUser;

추천 방식 정리[편집 | 원본 편집]

상황 추천 방식
학습용, 간단한 앱 순수 OAuth2 직접 구현
Firestore/Storage 사용 Firebase + Custom Token (백엔드 필요)
이미 백엔드 서버 있음 백엔드에서 JWT 발급 (Firebase 불필요)
Google/Apple만 사용 Firebase Auth 공식 (제일 간단)

주의사항[편집 | 원본 편집]

보안[편집 | 원본 편집]

  • Client Secret은 절대 앱 코드에 하드코딩 금지!
    • 백엔드 서버나 환경변수로 관리
    • 또는 Firebase Functions 사용

토큰 관리[편집 | 원본 편집]

  • Access Token은 flutter_secure_storage로 암호화 저장
  • Refresh Token으로 자동 갱신 구현 권장

Deep Link[편집 | 원본 편집]

  • Android: AndroidManifest.xml 설정
  • iOS: Info.plist + URL Scheme 설정
  • 웹: Callback URL을 앱 도메인으로 설정

패키지 활용[편집 | 원본 편집]

직접 구현이 복잡하다면 커뮤니티 패키지 사용:

  • 카카오: kakao_flutter_sdk (공식)
  • 네이버: flutter_naver_login
  • 디스코드: discord_oauth2
  • 범용 OAuth2: oauth2, flutter_appauth

하지만 학습 목적이라면 직접 구현 추천!