본문으로 이동

플러터:인증(OAuth2)

학교의 모든 지식. SMwiki
Sam (토론 | 기여)님의 2025년 12월 2일 (화) 01:25 판 (새 문서: {{플러터}} = 커스텀 OAuth2 인증 (네이버, 카카오 등) = Firebase는 Google/Apple/GitHub 등 주요 소셜 로그인만 공식 지원합니다. 네이버, 카카오, 디스코드 등의 OAuth2 인증은 '''직접 구현'''해야 합니다. == 방식 비교 == {| class="wikitable" !방식 !장점 !단점 !사용 케이스 |- |Firebase Auth 공식 |설정만으로 완성, 자동 토큰 관리 |지원 제공업체만 가능 |Google, Apple, GitHub, Microsoft |- |Fireba...)
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)

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

  1. 플러터:개요
    1. 플러터:VSCode
    2. 플러터:안드로이드 스튜디오
  2. 플러터:실행
  3. 플러터:개념 잡기
    1. 플러터:화면 하나 만들기
    2. 플러터:변하는 화면(StatefulWidget)
    3. 플러터:화면 전환(화면 쌓기, 하단 네비게이션 바)
    4. 플러터:화면 전환(Drawer)
    5. 플러터:입력 관련
      1. 플러터:버튼
      2. 플러터:키보드 입력
      3. 플러터:슬라이더
    6. 플러터:그래프 그리기(fl chart)
    7. 플러터:데이터 저장(간단한 데이터)
    8. 플러터:인증(Firebase 인증)(미완)
    9. 플러터:인증(OAuth2)(미완)
    10. 플러터:푸시 알람(FCM)(미완)
  4. 권한 사용
    1. 플러터:마이크 입력
  5. 위젯
    1. 플러터:아이콘
    2. 플러터:레이아웃 계열 위젯
    3. 플러터:네비게이션 계열 위젯
    4. 플러터:버튼
    5. 플러터:상태관리(미완)
  6. 플러터:DB연결
    1. 플러터:Firebase(미완)
    2. 플러터:MySQL(미완)
  7. 디자인
    1. 플러터:테마
    2. 플러터:앱바
  8. 플러터:배포
    1. 플러터:배포(안드로이드)(미완)
    2. 플러터:배포(iOS)(미완)
  9. 플러터:참고자료
  10. 플러터:위젯
    1. 플러터:공간배치용 위젯
  11. 플러터:구글 AdMob(미완)
  12. 플러터:라이브러리
    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으로 자동 갱신 구현 권장
  • Android: AndroidManifest.xml 설정
  • iOS: Info.plist + URL Scheme 설정
  • 웹: Callback URL을 앱 도메인으로 설정

패키지 활용

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

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

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