Ver3.5 모바일 앱 인증 시스템 완전 정복

모바일 앱 인증 시스템 완전 정복
진짜 개발자라면 알아야 할 인증의 모든 것 🔥
야 솔직히 말해서 앱 개발하다가 인증 시스템 구현할 때 제일 머리 아프지 않음? 😅
"그냥 아이디/비번 DB에 저장하면 되는 거 아니야?" 라고 생각했다가 보안 이슈 터지면 그날로 개발자 인생 끝날 수도 있음ㅋㅋㅋ
오늘은 OAuth 2.0, JWT, Firebase Authentication 이 세 가지 핵심 인증 기술을 완전히 뜯어보면서, 실제로 모바일 앱에서 어떻게 쓰이는지 낱낱이 파헤쳐볼 거임.
재능넷 같은 플랫폼에서도 사용자 인증은 핵심 중의 핵심이거든. 그만큼 중요한 내용이니까 집중해서 읽어봐! 💪
🔑 1. OAuth 2.0 — 권한 위임의 제왕
OAuth 2.0이 뭔지 한 줄로 설명하면?
"내 비밀번호 안 알려줘도 네가 내 대신 뭔가 할 수 있게 해주는 프로토콜"
예를 들어 카카오 로그인 버튼 눌렀을 때 "이 앱이 내 카카오 프로필 정보에 접근하려 합니다. 허용하시겠어요?" 이런 창 뜨잖아?
그게 바로 OAuth 2.0 동작하는 거임ㅋㅋㅋ 진짜 우리 일상에서 엄청 많이 쓰이는 기술이야.
RFC 6749로 표준화된 권한 부여 프레임워크(Authorization Framework)야.
인증(Authentication)이 아니라 인가(Authorization)가 핵심이라는 거 기억해! 이 차이 엄청 중요함.
📋 OAuth 2.0의 핵심 구성 요소
OAuth 2.0에는 4가지 역할이 있어:
바로 우리 사용자야. 카카오 계정 가진 사람. 자기 데이터에 대한 접근 권한을 가지고 있음.
우리가 만드는 앱! 사용자 대신 리소스에 접근하려는 애플리케이션.
카카오, 구글, 네이버 같은 곳. 토큰 발급해주는 서버.
실제 데이터가 있는 서버. 카카오 프로필 API 같은 거.
🔄 Authorization Code Flow — 가장 안전한 방식
모바일 앱에서 제일 많이 쓰이는 플로우야. 단계별로 보면:
📱 PKCE — 모바일 앱의 필수 보안 확장
모바일 앱에서 OAuth 2.0 쓸 때 PKCE(Proof Key for Code Exchange)는 선택이 아니라 필수야!
왜냐고? 모바일 앱은 client_secret을 안전하게 저장할 수가 없거든. 앱 바이너리 뜯으면 다 나와버림ㅋㅋㅋ
PKCE는 RFC 7636으로 표준화된 방식으로, 이렇게 동작해:
① code_verifier 생성
43~128자의 랜덤 문자열 생성. 예: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
② code_challenge 생성
code_challenge = BASE64URL(SHA256(code_verifier))
code_verifier를 SHA256 해시한 뒤 Base64URL 인코딩
③ 인가 요청 시 code_challenge 포함
?code_challenge=xxx&code_challenge_method=S256
④ 토큰 요청 시 code_verifier 포함
서버가 code_verifier로 code_challenge를 검증 → 중간자 공격 방어!
// Flutter에서 PKCE 구현 예시
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
String generateCodeVerifier() {
final random = Random.secure();
final values = List<int>.generate(32, (i) => random.nextInt(256));
return base64UrlEncode(values).replaceAll('=', '');
}
String generateCodeChallenge(String verifier) {
final bytes = utf8.encode(verifier);
final digest = sha256.convert(bytes);
return base64UrlEncode(digest.bytes).replaceAll('=', '');
}
// 사용
final verifier = generateCodeVerifier();
final challenge = generateCodeChallenge(verifier);
// challenge를 인가 요청에 포함, verifier는 로컬 저장
🎯 OAuth 2.0 Grant Types 비교
✅ 가장 안전
✅ 모바일/웹 앱 표준
✅ PKCE와 함께 사용
❌ 구현 복잡도 높음
✅ 서버 간 통신
✅ 빠른 구현
❌ 사용자 없는 경우만
❌ 모바일 앱 부적합
⚠️ 현재 비권장
⚠️ 보안 취약점 존재
❌ 새 앱에 사용 금지
❌ RFC에서 제거 예정
✅ TV/IoT 기기용
✅ 브라우저 없는 환경
✅ QR코드 로그인
❌ 일반 모바일 부적합
2019년 OAuth 2.0 Security Best Current Practice에서 공식적으로 비권장 처리됨.
Access Token이 URL fragment에 노출되는 심각한 보안 문제가 있어. 레거시 코드에 있다면 지금 당장 Authorization Code + PKCE로 마이그레이션해야 함!
🔄 토큰 갱신 전략
Access Token은 보통 1시간 정도 유효기간이 짧아. 그래서 Refresh Token으로 갱신하는 로직이 필요함:
// Android Kotlin - 토큰 갱신 예시
class TokenManager(private val context: Context) {
private val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
fun getAccessToken(): String? = prefs.getString("access_token", null)
fun getRefreshToken(): String? = prefs.getString("refresh_token", null)
suspend fun refreshAccessToken(): Result<String> {
val refreshToken = getRefreshToken()
?: return Result.failure(Exception("리프레시 토큰 없음"))
return try {
val response = authApi.refreshToken(
grantType = "refresh_token",
refreshToken = refreshToken,
clientId = BuildConfig.CLIENT_ID
)
// 새 토큰 저장
prefs.edit()
.putString("access_token", response.accessToken)
.putString("refresh_token", response.refreshToken)
.apply()
Result.success(response.accessToken)
} catch (e: Exception) {
// 리프레시 토큰도 만료 → 재로그인 필요
clearTokens()
Result.failure(e)
}
}
fun clearTokens() {
prefs.edit().clear().apply()
}
}
🎫 2. JWT — 토큰 그 자체가 정보다
JWT(JSON Web Token)는 RFC 7519로 표준화된 토큰 형식이야.
OAuth 2.0이 "어떻게 토큰을 발급할 것인가"에 대한 프로토콜이라면,
JWT는 "토큰을 어떤 형식으로 만들 것인가"에 대한 스펙이라고 보면 됨.
JWT의 가장 큰 특징은 토큰 자체에 정보가 담겨있다는 거야.
서버가 DB 조회 없이 토큰만 보고 "이 사람 누구인지, 어떤 권한 있는지" 알 수 있음. 개쩔지 않음?ㅋㅋㅋ
🏗️ JWT 구조 완전 분해
JWT는 점(.)으로 구분된 세 파트로 이루어져 있어:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6Iu2YhOuLpCIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
🔵 헤더(Header) — 알고리즘과 토큰 타입
🟢 페이로드(Payload) — 실제 데이터(클레임)
🔴 서명(Signature) — 위변조 방지
🔐 서명 알고리즘: HS256 vs RS256
대칭키 방식
✅ 빠른 처리 속도
✅ 구현 간단
❌ 서명·검증에 같은 키 사용
❌ 마이크로서비스 환경 부적합
→ 단일 서버 환경에 적합
비대칭키 방식
✅ 공개키로 누구나 검증
✅ 마이크로서비스 최적
✅ 키 노출 위험 낮음
❌ 처리 속도 상대적 느림
→ 분산 시스템에 적합
Firebase Authentication, Google OAuth 등 대형 서비스들은 대부분 RS256을 사용해.
공개키(JWKS endpoint)를 공개해서 누구나 토큰 검증 가능하게 하는 방식이야.
예:
https://www.googleapis.com/oauth2/v3/certs
⚡ JWT 검증 로직 구현
// iOS Swift - JWT 검증 예시 (JWTDecode 라이브러리 활용)
import JWTDecode
class JWTValidator {
func validateToken(_ tokenString: String) -> Result<JWTPayload, AuthError> {
do {
let jwt = try decode(jwt: tokenString)
// 1. 만료 시간 검증
guard let expiresAt = jwt.expiresAt,
expiresAt > Date() else {
return .failure(.tokenExpired)
}
// 2. 발급자 검증
guard jwt.issuer == "https://your-auth-server.com" else {
return .failure(.invalidIssuer)
}
// 3. 대상자 검증
guard jwt.audience?.contains("your-app-id") == true else {
return .failure(.invalidAudience)
}
// 4. 클레임 추출
let payload = JWTPayload(
userId: jwt.subject ?? "",
email: jwt.claim(name: "email").string ?? "",
role: jwt.claim(name: "role").string ?? "user"
)
return .success(payload)
} catch {
return .failure(.invalidToken)
}
}
}
// 서명 검증은 서버사이드에서 수행 (클라이언트에서 서명 검증은 공개키 필요)
// 모바일 앱에서는 HTTPS + 서버 검증이 기본 원칙!
⚠️ JWT 사용 시 절대 하면 안 되는 것들
1. 페이로드에 민감 정보 넣지 마!
JWT 페이로드는 Base64URL 인코딩이지 암호화가 아님. 누구나 디코딩 가능!
비밀번호, 카드번호, 주민번호 절대 금지 🚫
2. "alg: none" 공격 주의!
일부 라이브러리가 alg를 none으로 설정하면 서명 검증을 건너뜀.
반드시 허용 알고리즘 화이트리스트 설정 필수!
3. 토큰 저장 위치 신중하게!
웹: localStorage 금지(XSS 취약), httpOnly 쿠키 권장
모바일: Android Keystore / iOS Keychain 사용 필수
4. 짧은 만료 시간 설정!
Access Token: 15분~1시간
Refresh Token: 7일~30일 (Rotation 전략 적용)
🔄 JWT Refresh Token Rotation 전략
Refresh Token 탈취 방지를 위한 Rotation 전략이 현재 업계 표준이야:
// 서버사이드 - Refresh Token Rotation 구현 (Node.js)
async function refreshTokens(refreshToken) {
// 1. DB에서 리프레시 토큰 조회
const storedToken = await db.refreshTokens.findOne({
token: refreshToken,
isRevoked: false
});
if (!storedToken) {
// 이미 사용된 토큰 → 토큰 탈취 의심!
// 해당 사용자의 모든 토큰 무효화
await db.refreshTokens.updateMany(
{ userId: storedToken?.userId },
{ isRevoked: true }
);
throw new Error('토큰 재사용 감지 - 보안 위협!');
}
// 2. 만료 시간 확인
if (storedToken.expiresAt < new Date()) {
throw new Error('리프레시 토큰 만료');
}
// 3. 기존 토큰 무효화
await db.refreshTokens.updateOne(
{ _id: storedToken._id },
{ isRevoked: true }
);
// 4. 새 토큰 쌍 발급
const newAccessToken = generateAccessToken(storedToken.userId);
const newRefreshToken = generateRefreshToken();
// 5. 새 리프레시 토큰 저장
await db.refreshTokens.create({
token: newRefreshToken,
userId: storedToken.userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30일
});
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
🔥 3. Firebase Authentication — 빠르고 강력한 올인원
Firebase Authentication은 구글이 만든 BaaS(Backend as a Service) 인증 솔루션이야.
OAuth 2.0이나 JWT를 직접 구현하는 게 너무 복잡하다면? Firebase Auth가 답임ㅋㅋㅋ
근데 그냥 쓰기만 하면 안 되고, 내부 동작 원리도 알아야 제대로 활용할 수 있어!
이메일/비밀번호 · 전화번호(SMS) · 구글 · 애플 · 페이스북 · 트위터 · 깃허브 · 마이크로소프트 · 야후 · 익명 로그인 · 커스텀 토큰
이 모든 걸 하나의 SDK로 처리할 수 있다는 게 Firebase의 최대 장점!
🏗️ Firebase Auth 내부 동작 원리
Firebase Auth는 내부적으로 Google Identity Platform을 기반으로 동작해.
인증 성공 시 Firebase가 발급하는 토큰은 사실 JWT 형식이야!
Firebase ID Token 구조:
iss https://securetoken.google.com/[PROJECT_ID]
sub Firebase UID (사용자 고유 식별자)
aud Firebase Project ID
exp 발급 후 1시간 유효
email 사용자 이메일
email_verified 이메일 인증 여부
firebase 로그인 제공자 정보
📱 Flutter에서 Firebase Auth 완전 구현
// pubspec.yaml
dependencies:
firebase_core: ^2.24.0
firebase_auth: ^4.15.0
google_sign_in: ^6.1.6
// main.dart - Firebase 초기화
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
// auth_service.dart - 인증 서비스
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn();
// 현재 사용자 스트림
Stream<User?> get authStateChanges => _auth.authStateChanges();
// 이메일/비밀번호 회원가입
Future<UserCredential> signUpWithEmail(String email, String password) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// 이메일 인증 메일 발송
await credential.user?.sendEmailVerification();
return credential;
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
}
}
// 구글 로그인
Future<UserCredential?> signInWithGoogle() async {
try {
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) return null; // 사용자가 취소
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return await _auth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
}
}
// 전화번호 인증 (SMS)
Future<void> verifyPhoneNumber({
required String phoneNumber,
required Function(PhoneAuthCredential) onVerificationCompleted,
required Function(FirebaseAuthException) onVerificationFailed,
required Function(String, int?) onCodeSent,
}) async {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumber, // '+82 10-1234-5678'
verificationCompleted: onVerificationCompleted,
verificationFailed: onVerificationFailed,
codeSent: onCodeSent,
codeAutoRetrievalTimeout: (verificationId) {},
timeout: const Duration(seconds: 60),
);
}
// ID Token 가져오기 (서버 검증용)
Future<String?> getIdToken({bool forceRefresh = false}) async {
return await _auth.currentUser?.getIdToken(forceRefresh);
}
// 로그아웃
Future<void> signOut() async {
await Future.wait([
_auth.signOut(),
_googleSignIn.signOut(),
]);
}
// 에러 처리
String _handleAuthException(FirebaseAuthException e) {
switch (e.code) {
case 'weak-password': return '비밀번호가 너무 약합니다 (6자 이상)';
case 'email-already-in-use': return '이미 사용 중인 이메일입니다';
case 'invalid-email': return '유효하지 않은 이메일 형식입니다';
case 'user-not-found': return '등록되지 않은 이메일입니다';
case 'wrong-password': return '비밀번호가 틀렸습니다';
case 'too-many-requests': return '너무 많은 시도. 잠시 후 다시 시도해주세요';
default: return '인증 오류: ${e.message}';
}
}
}
🛡️ Firebase ID Token 서버 검증
Firebase Auth를 쓴다고 해서 서버 검증을 생략하면 안 돼!
클라이언트에서 받은 ID Token을 서버에서 반드시 검증해야 함:
// 서버사이드 - Firebase Admin SDK로 토큰 검증 (Node.js)
const admin = require('firebase-admin');
// Firebase Admin 초기화 (서비스 계정 키 사용)
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
});
// 미들웨어 - 모든 보호된 라우트에 적용
async function verifyFirebaseToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: '인증 토큰이 없습니다' });
}
const idToken = authHeader.split('Bearer ')[1];
try {
const decodedToken = await admin.auth().verifyIdToken(idToken);
// 이메일 인증 여부 확인 (선택적)
if (!decodedToken.email_verified) {
return res.status(403).json({ error: '이메일 인증이 필요합니다' });
}
req.user = {
uid: decodedToken.uid,
email: decodedToken.email,
name: decodedToken.name,
};
next();
} catch (error) {
if (error.code === 'auth/id-token-expired') {
return res.status(401).json({ error: '토큰이 만료되었습니다' });
}
return res.status(401).json({ error: '유효하지 않은 토큰' });
}
}
// 보호된 라우트 예시
app.get('/api/profile', verifyFirebaseToken, async (req, res) => {
const userProfile = await db.users.findOne({ uid: req.user.uid });
res.json(userProfile);
});
🔒 Firebase Security Rules — 데이터 보안의 핵심
Firebase Firestore나 Realtime Database를 쓴다면 Security Rules 설정이 필수야!
이거 제대로 안 하면 데이터 다 털릴 수 있음 진짜로ㅋㅋㅋ
// Firestore Security Rules 예시
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 사용자 프로필 - 본인만 읽기/쓰기 가능
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
// 게시글 - 인증된 사용자만 읽기, 작성자만 수정/삭제
match /posts/{postId} {
allow read: if request.auth != null;
allow create: if request.auth != null
&& request.resource.data.authorId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.authorId == request.auth.uid;
}
// 관리자 전용 컬렉션
match /admin/{document=**} {
allow read, write: if request.auth != null
&& request.auth.token.admin == true;
}
// 공개 데이터 - 누구나 읽기 가능
match /public/{document=**} {
allow read: if true;
allow write: if request.auth != null;
}
}
}
Firebase Admin SDK로 사용자에게 커스텀 클레임을 추가할 수 있어.
예:
admin.auth().setCustomUserClaims(uid, { admin: true, role: 'moderator' })이 클레임은 ID Token에 포함되어 Security Rules에서
request.auth.token.admin으로 접근 가능!
⚡ 4. 세 기술의 조합 — 실전 아키텍처
실제 프로덕션 앱에서는 이 세 기술이 따로 놀지 않아.
서로 유기적으로 연결되어 동작하는 게 일반적이야.
재능넷 같은 플랫폼을 예시로 실전 아키텍처를 설계해보자!
📱 생체 인증(Biometric) 연동
요즘 앱들은 지문이나 Face ID로 로그인하잖아?
이건 OAuth/JWT를 대체하는 게 아니라 로컬 인증 레이어를 추가하는 거야.
내부적으로는 여전히 JWT 토큰으로 서버 통신함!
// Flutter - 생체 인증 구현
import 'package:local_auth/local_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class BiometricAuthService {
final LocalAuthentication _localAuth = LocalAuthentication();
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
// 생체 인증 가능 여부 확인
Future<bool> isBiometricAvailable() async {
final isAvailable = await _localAuth.canCheckBiometrics;
final isDeviceSupported = await _localAuth.isDeviceSupported();
return isAvailable && isDeviceSupported;
}
// 사용 가능한 생체 인증 타입 확인
Future<List<BiometricType>> getAvailableBiometrics() async {
return await _localAuth.getAvailableBiometrics();
// [BiometricType.fingerprint, BiometricType.face, BiometricType.iris]
}
// 생체 인증 실행
Future<bool> authenticate() async {
try {
return await _localAuth.authenticate(
localizedReason: '앱에 접근하려면 생체 인증이 필요합니다',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: false, // PIN도 허용
),
);
} catch (e) {
return false;
}
}
// 생체 인증 성공 후 저장된 토큰 가져오기
Future<String?> getTokenWithBiometric() async {
final authenticated = await authenticate();
if (!authenticated) return null;
// Keychain/Keystore에서 안전하게 토큰 읽기
return await _secureStorage.read(key: 'refresh_token');
}
// 토큰 안전 저장 (최초 로그인 시)
Future<void> saveTokenSecurely(String token) async {
await _secureStorage.write(
key: 'refresh_token',
value: token,
// iOS: Keychain에 저장, Android: EncryptedSharedPreferences
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
aOptions: const AndroidOptions(
encryptedSharedPreferences: true,
),
);
}
}
🌐 OIDC — OAuth 2.0 위에 인증 레이어 추가
OpenID Connect(OIDC)는 OAuth 2.0 위에 인증(Authentication) 레이어를 추가한 프로토콜이야.
OAuth 2.0이 "권한 위임"이라면, OIDC는 "신원 확인"까지 해주는 거임.
Firebase Authentication도 내부적으로 OIDC를 구현하고 있어!
OIDC가 OAuth 2.0에 추가하는 것들:
ID Token 사용자 신원 정보가 담긴 JWT
UserInfo Endpoint 추가 사용자 정보 조회 API
Discovery Document 서버 설정 자동 검색
nonce 리플레이 공격 방지
JWKS URI 공개키 자동 조회
🛡️ 5. 보안 심화 — 진짜 프로는 이것까지 안다
🔒 Certificate Pinning (인증서 피닝)
HTTPS를 쓴다고 해서 무조건 안전한 건 아님.
중간자 공격(MITM)을 막으려면 Certificate Pinning이 필요해!
// Android - OkHttp Certificate Pinning
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("api.yourapp.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.yourapp.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 백업 핀
.build()
)
.build()
// iOS - URLSession Certificate Pinning
class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 서버 인증서와 로컬 저장 인증서 비교
let pinnedCertificates = loadPinnedCertificates()
let serverCertificates = getServerCertificates(from: serverTrust)
if pinnedCertificates.contains(where: { serverCertificates.contains($0) }) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
🚨 주요 공격 유형과 방어 전략
📊 세 기술 최종 비교 — 언제 뭘 써야 하나?
🎯 상황별 추천 전략:
① 스타트업 / 빠른 MVP 개발
→ Firebase Authentication 추천!
구현 시간 최소화, 소셜 로그인 즉시 지원, 보안 걱정 덜함.
단, Firebase 의존성 생기는 거 감안해야 함.
② 대규모 서비스 / 마이크로서비스 아키텍처
→ OAuth 2.0 + JWT (RS256) 직접 구현 추천!
완전한 제어권, 벤더 락인 없음, 커스터마이징 자유도 높음.
단, 구현 복잡도 높고 보안 책임도 직접 져야 함.
③ 기존 Firebase + 커스텀 백엔드 연동
→ Firebase Auth + Custom Token 조합!
Firebase ID Token을 서버에서 검증 후 커스텀 JWT 발급.
두 세계의 장점을 모두 활용 가능.
④ 엔터프라이즈 / 사내 시스템
→ SAML 2.0 또는 OIDC 기반 SSO 구현!
Active Directory, Okta, Azure AD 등과 연동.
Firebase Auth도 SAML 제공자 지원함.
🔧 실전 코드: 완전한 인증 인터셉터 구현
// Flutter - Dio HTTP 인터셉터로 자동 토큰 갱신
import 'package:dio/dio.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AuthInterceptor extends Interceptor {
final Dio _dio;
bool _isRefreshing = false;
final List<RequestOptions> _pendingRequests = [];
AuthInterceptor(this._dio);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// Firebase ID Token 자동 첨부
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
// forceRefresh: false → 캐시된 토큰 사용 (만료 5분 전 자동 갱신)
final token = await user.getIdToken();
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
if (_isRefreshing) {
// 갱신 중이면 대기열에 추가
_pendingRequests.add(err.requestOptions);
return;
}
_isRefreshing = true;
try {
// Firebase 토큰 강제 갱신
final user = FirebaseAuth.instance.currentUser;
if (user == null) throw Exception('로그인 필요');
final newToken = await user.getIdToken(true); // forceRefresh: true
// 대기 중인 요청들 재시도
for (final request in _pendingRequests) {
request.headers['Authorization'] = 'Bearer $newToken';
await _dio.fetch(request);
}
_pendingRequests.clear();
// 원래 요청 재시도
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await _dio.fetch(err.requestOptions);
handler.resolve(response);
} catch (e) {
// 토큰 갱신 실패 → 로그아웃 처리
await FirebaseAuth.instance.signOut();
handler.reject(err);
} finally {
_isRefreshing = false;
}
} else {
handler.next(err);
}
}
}
// Dio 설정
final dio = Dio(BaseOptions(baseUrl: 'https://api.yourapp.com'));
dio.interceptors.add(AuthInterceptor(dio));
🎯 Apple Sign In — iOS 앱의 필수 요소
App Store에 소셜 로그인(구글, 카카오 등)이 있는 앱을 출시하려면
Apple Sign In을 반드시 함께 제공해야 해! Apple 정책임.
이거 안 지키면 앱 심사 거절당함ㅋㅋㅋ
// iOS Swift - Apple Sign In + Firebase 연동
import AuthenticationServices
import FirebaseAuth
class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate {
private var currentNonce: String?
func startSignInWithApple() {
let nonce = randomNonceString()
currentNonce = nonce
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce) // nonce 해시 전달
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()
}
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
let nonce = currentNonce,
let appleIDToken = appleIDCredential.identityToken,
let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
return
}
// Firebase Credential 생성
let credential = OAuthProvider.appleCredential(
withIDToken: idTokenString,
rawNonce: nonce,
fullName: appleIDCredential.fullName
)
// Firebase 로그인
Auth.auth().signIn(with: credential) { authResult, error in
if let error = error {
print("Apple 로그인 실패: \(error.localizedDescription)")
return
}
print("Apple 로그인 성공! UID: \(authResult?.user.uid ?? "")")
}
}
// 랜덤 nonce 생성
private func randomNonceString(length: Int = 32) -> String {
let charset = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0..<16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
return errorCode == errSecSuccess ? random : 0
}
randoms.forEach { random in
if remainingLength == 0 { return }
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
}
📚 6. 실무에서 자주 마주치는 문제들
❓ Q&A 형식으로 알아보는 실전 이슈들
Q1. 토큰 만료 시간을 얼마로 설정해야 하나요?
Access Token: 15분~1시간 (보안 민감 서비스는 15분, 일반 서비스는 1시간)
Refresh Token: 7일~30일 (사용자 경험 vs 보안 트레이드오프)
Firebase ID Token: 1시간 고정 (변경 불가, SDK가 자동 갱신)
💡 팁: 금융 앱이면 Access Token 15분, Refresh Token 24시간 권장!
Q2. 앱이 백그라운드에 있을 때 토큰 갱신은 어떻게?
Firebase Auth SDK는 자동으로 처리해줌. 앱이 포그라운드로 돌아올 때 만료된 토큰을 자동 갱신.
직접 구현 시에는 WorkManager(Android) / BackgroundTasks(iOS)를 활용해서
백그라운드 토큰 갱신 작업을 스케줄링할 수 있어.
Q3. 여러 기기에서 동시 로그인 제어는?
Firebase Auth는 기본적으로 멀티 디바이스 로그인 허용.
단일 기기 로그인 강제하려면:
1. 로그인 시 서버 DB에 현재 기기 ID 저장
2. 다른 기기에서 로그인 시 기존 기기의 Refresh Token 무효화
3. Firebase Custom Token으로 세션 관리 구현
또는 Firebase의 revoke refresh tokens API 활용:
admin.auth().revokeRefreshTokens(uid)
Q4. 카카오 로그인 + Firebase 연동은 어떻게?
카카오는 Firebase에서 기본 제공 안 함. 이렇게 해야 해:
1. 카카오 SDK로 카카오 로그인 → 카카오 Access Token 획득
2. 서버에 카카오 Access Token 전송
3. 서버에서 카카오 API로 사용자 정보 조회
4. Firebase Admin SDK로 Custom Token 생성
5. 클라이언트에서 Custom Token으로 Firebase 로그인
→ 이렇게 하면 카카오 사용자도 Firebase UID 발급받음!
Q5. JWT 토큰을 강제로 무효화(로그아웃)하려면?
JWT의 가장 큰 단점이 바로 이거야. Stateless라서 서버에서 즉시 무효화가 어려움.
해결 방법들:
① 블랙리스트 방식: 무효화된 jti를 Redis에 저장, 요청마다 확인 (성능 저하)
② 짧은 만료 시간: Access Token 15분 → 최대 15분 후 자동 무효화
③ 버전 관리: DB에 token_version 저장, 토큰의 버전과 비교
④ Firebase 방식: admin.auth().revokeRefreshTokens(uid) 호출 후
ID Token 검증 시 checkRevoked: true 옵션 사용
🎓 마무리 — 인증 시스템 선택 가이드
지금까지 OAuth 2.0, JWT, Firebase Authentication을 깊게 파봤어.
재능넷처럼 다양한 사용자가 모이는 플랫폼에서는 인증 시스템이 정말 중요한 기반이 되거든.
마지막으로 핵심 포인트만 정리해줄게!
OAuth 2.0 = 권한 위임 프로토콜. "내 대신 이 앱이 이것만 할 수 있게 해줘"
→ 소셜 로그인의 기반, PKCE 필수, Authorization Code Flow 사용
JWT = 자가 포함 토큰 형식. "이 토큰 자체에 내 정보가 다 있어"
→ Stateless 인증, RS256 권장, 민감정보 절대 포함 금지
Firebase Auth = 올인원 인증 플랫폼. "이거 하나로 다 해결"
→ 빠른 개발, 다양한 로그인 방식, 내부적으로 JWT 사용, 서버 검증 필수
1. 클라이언트(앱)에서 JWT 서명 검증은 공개키 필요 → 서버에서 검증이 원칙
2. 토큰은 반드시 Keychain/Keystore에 저장 (SharedPreferences/UserDefaults 금지!)
3. HTTPS 없이 토큰 전송 절대 금지
4. 페이로드에 비밀번호, 카드번호 등 민감 정보 절대 금지
5. 프로덕션 환경에서 Firebase 보안 규칙 반드시 설정 (기본값은 모든 접근 허용!)
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.