본문으로 이동

플러터:인증(OAuth2): 두 판 사이의 차이

학교의 모든 지식. SMwiki
새 문서: {{플러터}} = 커스텀 OAuth2 인증 (네이버, 카카오 등) = Firebase는 Google/Apple/GitHub 등 주요 소셜 로그인만 공식 지원합니다. 네이버, 카카오, 디스코드 등의 OAuth2 인증은 '''직접 구현'''해야 합니다. == 방식 비교 == {| class="wikitable" !방식 !장점 !단점 !사용 케이스 |- |Firebase Auth 공식 |설정만으로 완성, 자동 토큰 관리 |지원 제공업체만 가능 |Google, Apple, GitHub, Microsoft |- |Fireba...
 
 
(같은 사용자의 중간 판 하나는 보이지 않습니다)
1번째 줄: 1번째 줄:
{{플러터}}
{{플러터}}


= 커스텀 OAuth2 인증 (네이버, 카카오 등) =
= [아직 미완의 문서입니다.] =
Firebase는 Google/Apple/GitHub 등 주요 소셜 로그인만 공식 지원합니다. 네이버, 카카오, 디스코드 등의 OAuth2 인증은 '''직접 구현'''해야 합니다.


== 방식 비교 ==
= 개요 =
{| class="wikitable"
 
!방식
* Firebase는 Google/Apple/GitHub 등 주요 소셜 로그인만 공식 지원함.
!장점
* 이 문서에선 일반적인 표준 OAuth2 규격을 100% 준수(Authorization Code + PKCE)하는 경우의 로그인을 구현함.(flutter_appauth 사용)
!단점
* Django(OAuth2 Provider)·Auth0·Keycloak 등 OAuth2 표준을 준수하는 서버라면 모두 동일한 방식으로 연동 가능함.
!사용 케이스
 
|-
=== 비고 ===
|Firebase Auth 공식
|설정만으로 완성, 자동 토큰 관리
|지원 제공업체만 가능
|Google, Apple, GitHub, Microsoft
|-
|Firebase + Custom Token
|Firebase 생태계 유지
|백엔드 서버 필요
|네이버/카카오 + Firestore 함께 사용
|-
|순수 OAuth2 직접 구현
|완전한 제어, 백엔드 불필요
|토큰/세션 직접 관리
|간단한 앱, 학습용
|}


== 네이버 OAuth2 예시 (순수 구현) ==
* 네이버, 카카오, 디스코드 등의 인증은 OAuth2와 유사하지만 표준을 완전히 따르지 않기 때문에 직접 구현해야 하는데, 네이버, 카카오는 일반 OAuth2의 규격과 조금 다름. 아래 패키지를 이용하면 앱투앱 로그인도 간편하게 이루어짐.
* 카카오: <code>kakao_flutter_sdk</code> (공식)
* 네이버: <code>flutter_naver_login</code>
* 디스코드: <code>discord_oauth2</code>
= 사전 준비 =


=== 사전 준비 ===
== 필수 개념 ==
* Authorization Code Flow with PKCE
* Authorization Endpoint: <code>/authorize</code>
* Token Endpoint: <code>/token</code>
* Client ID / Redirect URI
{| class="wikitable"
{| class="wikitable"
!단계
|+
!할 일
!설명
!설명
!비고
|-
|-
|네이버 개발자센터 설정
|<code>pubspec.yaml</code>에 패키지 추가
|
|
# https://developers.naver.com/apps 접속
# 애플리케이션 등록
# '''Client ID''', '''Client Secret''' 발급
# '''Callback URL''' 설정: <code>http://localhost/callback</code> (웹) 또는 커스텀 스킴
|-
|패키지 설치
|<syntaxhighlight lang="yaml">
|<syntaxhighlight lang="yaml">
dependencies:
dependencies:
   http: ^1.1.0             # HTTP 요청용
   flutter_appauth: ^5.0.0
  flutter_secure_storage: ^9.0.0  # 토큰 안전 저장
  url_launcher: ^6.2.1      # OAuth 웹뷰 열기
</syntaxhighlight>
</syntaxhighlight>
|-
|Redirect URI 등록
|[스킴 지정하는 법에 대해선 따로 찾아보아야 할듯.]
|Flutter 앱은 보통 스킴 기반 URI를 사용함.
|-
|
|
|}
|}


=== OAuth2 흐름 ===
= Flutter 코드 (최소 구현 예제) =
 
# 사용자가 "네이버 로그인" 버튼 클릭
# 네이버 로그인 페이지로 이동 (브라우저/웹뷰)
# 사용자 로그인 → 네이버가 '''Authorization Code''' 발급
# 앱이 Code를 받아서 '''Access Token''' 요청
# Access Token으로 사용자 정보 API 호출


=== 구현 코드 ===
== 인증 요청 ==
<syntaxhighlight lang="dart">
<syntaxhighlight lang="dart">
import 'package:flutter/material.dart';
import 'package:flutter_appauth/flutter_appauth.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)) {
final appAuth = FlutterAppAuth();
      await launchUrl(authUrl, mode: LaunchMode.externalApplication);
     
      // 실제로는 Callback을 받아야 함 (아래 참고)
      // 웹: window.location 감지
      // 모바일: Deep Link 설정 필요
    }
  }


  // 2단계: Authorization Code로 Access Token 받기
Future<void> login() async {
  Future<String?> getAccessToken(String code) async {
  final result = await appAuth.authorizeAndExchangeCode(
    final response = await http.post(
    AuthorizationTokenRequest(
      Uri.parse('https://nid.naver.com/oauth2.0/token'),
       "your_client_id",
       body: {
       "com.example.app:/oauth",
        'grant_type': 'authorization_code',
       serviceConfiguration: AuthorizationServiceConfiguration(
        'client_id': clientId,
        authorizationEndpoint: "https://your-server.com/o/authorize/",
        'client_secret': clientSecret,
        tokenEndpoint: "https://your-server.com/o/token/",
        '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('로그아웃'),
                  ),
                ],
              ),
       ),
       ),
     );
      scopes: ['openid', 'profile', 'email'],
   }
     ),
}
   );
</syntaxhighlight>


=== Callback 처리 (중요!) ===
  print("Access Token: ${result?.accessToken}");
 
==== 웹 ====
<syntaxhighlight lang="dart">
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());
}
</syntaxhighlight>
 
==== Android (Deep Link) ====
<code>android/app/src/main/AndroidManifest.xml</code>:<syntaxhighlight lang="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>
</syntaxhighlight>Flutter에서 받기:<syntaxhighlight lang="dart">
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);
      }
    }
  });
}
}
</syntaxhighlight>
</syntaxhighlight>


== Firebase Custom Token 방식 ==
== 토큰 갱신 ==
백엔드 서버가 있다면 더 안전한 방식:
 
=== 흐름 ===
 
# Flutter → 네이버 OAuth로 Access Token 받기
# Flutter → 백엔드 서버로 Access Token 전송
# 백엔드 → 네이버 API로 사용자 정보 확인
# 백엔드 → '''Firebase Admin SDK'''로 Custom Token 생성
# 백엔드 → Flutter로 Custom Token 전송
# Flutter → Firebase.signInWithCustomToken()으로 로그인
 
=== 장점 ===
 
* Client Secret이 앱에 노출 안 됨 (보안 ↑)
* Firebase Authentication + Firestore 연동 가능
* Firebase Security Rules 사용 가능
 
=== 백엔드 예시 (Node.js) ===
<syntaxhighlight lang="javascript">
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 });
});
</syntaxhighlight>
 
=== Flutter에서 사용 ===
<syntaxhighlight lang="dart">
<syntaxhighlight lang="dart">
// 1. 네이버 로그인 → Access Token 받기
final refreshed = await appAuth.token(
final accessToken = await getNaverAccessToken();
  TokenRequest(
 
    "your_client_id",
// 2. 백엔드로 전송 → Custom Token 받기
    "com.example.app:/oauth",
final response = await http.post(
    refreshToken: result.refreshToken,
  Uri.parse('https://yourserver.com/auth/naver'),
    serviceConfiguration: AuthorizationServiceConfiguration(
   body: {'accessToken': accessToken},
      authorizationEndpoint: "https://your-server.com/o/authorize/",
      tokenEndpoint: "https://your-server.com/o/token/",
    ),
   ),
);
);
final customToken = json.decode(response.body)['customToken'];
// 3. Firebase 로그인
await FirebaseAuth.instance.signInWithCustomToken(customToken);
// 4. 이제 Firebase 기능 모두 사용 가능!
final user = FirebaseAuth.instance.currentUser;
</syntaxhighlight>
</syntaxhighlight>


== 추천 방식 정리 ==
= OAuth 제공자 설정 =
{| class="wikitable"
!상황
!추천 방식
|-
|학습용, 간단한 앱
|'''순수 OAuth2 직접 구현'''
|-
|Firestore/Storage 사용
|'''Firebase + Custom Token''' (백엔드 필요)
|-
|이미 백엔드 서버 있음
|백엔드에서 JWT 발급 (Firebase 불필요)
|-
|Google/Apple만 사용
|'''Firebase Auth 공식''' (제일 간단)
|}


== 주의사항 ==
== Django OAuth2 서버 연동 ==


=== 보안 ===
=== Django-OAuth-Toolkit 설정 ===
 
<syntaxhighlight lang="python">
* '''Client Secret'''은 절대 앱 코드에 하드코딩 금지!
OAUTH2_PROVIDER = {
** 백엔드 서버나 환경변수로 관리
    "ACCESS_TOKEN_EXPIRE_SECONDS": 3600,
** 또는 Firebase Functions 사용
    "PKCE_REQUIRED": True,
 
}
=== 토큰 관리 ===
</syntaxhighlight>
 
* Access Token은 <code>flutter_secure_storage</code>로 암호화 저장
* Refresh Token으로 자동 갱신 구현 권장
 
=== Deep Link ===
 
* Android: <code>AndroidManifest.xml</code> 설정
* iOS: <code>Info.plist</code> + URL Scheme 설정
* 웹: Callback URL을 앱 도메인으로 설정


== 패키지 활용 ==
=== Application 등록 ===
직접 구현이 복잡하다면 커뮤니티 패키지 사용:
* Client type: Public
* Authorization grant type: Authorization code
* Redirect URIs: <code>com.example.app:/oauth</code>


* '''카카오''': <code>kakao_flutter_sdk</code> (공식)
----
* '''네이버''': <code>flutter_naver_login</code>
* '''디스코드''': <code>discord_oauth2</code>
* '''범용 OAuth2''': <code>oauth2</code>, <code>flutter_appauth</code>


하지만 학습 목적이라면 '''직접 구현 추천'''!
=== 최소 동작 구조 요약 ===
# OAuth2 서버에서 Client 등록 (Redirect URI 포함)
# Flutter에서 <code>flutter_appauth</code>를 이용해 로그인
# OAuth 서버가 Authorization Code 발급
# Flutter가 Authorization Code → Access Token 교환
# 필요 시 Refresh Token으로 갱신

2025년 12월 10일 (수) 04:21 기준 최신판

틀:플러터 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

[아직 미완의 문서입니다.]

[편집 | 원본 편집]
  • Firebase는 Google/Apple/GitHub 등 주요 소셜 로그인만 공식 지원함.
  • 이 문서에선 일반적인 표준 OAuth2 규격을 100% 준수(Authorization Code + PKCE)하는 경우의 로그인을 구현함.(flutter_appauth 사용)
  • Django(OAuth2 Provider)·Auth0·Keycloak 등 OAuth2 표준을 준수하는 서버라면 모두 동일한 방식으로 연동 가능함.
  • 네이버, 카카오, 디스코드 등의 인증은 OAuth2와 유사하지만 표준을 완전히 따르지 않기 때문에 직접 구현해야 하는데, 네이버, 카카오는 일반 OAuth2의 규격과 조금 다름. 아래 패키지를 이용하면 앱투앱 로그인도 간편하게 이루어짐.
  • 카카오: kakao_flutter_sdk (공식)
  • 네이버: flutter_naver_login
  • 디스코드: discord_oauth2

사전 준비

[편집 | 원본 편집]

필수 개념

[편집 | 원본 편집]
  • Authorization Code Flow with PKCE
  • Authorization Endpoint: /authorize
  • Token Endpoint: /token
  • Client ID / Redirect URI
할 일 설명 비고
pubspec.yaml에 패키지 추가
dependencies:
  flutter_appauth: ^5.0.0
Redirect URI 등록 [스킴 지정하는 법에 대해선 따로 찾아보아야 할듯.] Flutter 앱은 보통 스킴 기반 URI를 사용함.

Flutter 코드 (최소 구현 예제)

[편집 | 원본 편집]

인증 요청

[편집 | 원본 편집]
import 'package:flutter_appauth/flutter_appauth.dart';

final appAuth = FlutterAppAuth();

Future<void> login() async {
  final result = await appAuth.authorizeAndExchangeCode(
    AuthorizationTokenRequest(
      "your_client_id",
      "com.example.app:/oauth",
      serviceConfiguration: AuthorizationServiceConfiguration(
        authorizationEndpoint: "https://your-server.com/o/authorize/",
        tokenEndpoint: "https://your-server.com/o/token/",
      ),
      scopes: ['openid', 'profile', 'email'],
    ),
  );

  print("Access Token: ${result?.accessToken}");
}

토큰 갱신

[편집 | 원본 편집]
final refreshed = await appAuth.token(
  TokenRequest(
    "your_client_id",
    "com.example.app:/oauth",
    refreshToken: result.refreshToken,
    serviceConfiguration: AuthorizationServiceConfiguration(
      authorizationEndpoint: "https://your-server.com/o/authorize/",
      tokenEndpoint: "https://your-server.com/o/token/",
    ),
  ),
);

OAuth 제공자 설정

[편집 | 원본 편집]

Django OAuth2 서버 연동

[편집 | 원본 편집]

Django-OAuth-Toolkit 설정

[편집 | 원본 편집]
OAUTH2_PROVIDER = {
    "ACCESS_TOKEN_EXPIRE_SECONDS": 3600,
    "PKCE_REQUIRED": True,
}

Application 등록

[편집 | 원본 편집]
  • Client type: Public
  • Authorization grant type: Authorization code
  • Redirect URIs: com.example.app:/oauth

최소 동작 구조 요약

[편집 | 원본 편집]
  1. OAuth2 서버에서 Client 등록 (Redirect URI 포함)
  2. Flutter에서 flutter_appauth를 이용해 로그인
  3. OAuth 서버가 Authorization Code 발급
  4. Flutter가 Authorization Code → Access Token 교환
  5. 필요 시 Refresh Token으로 갱신