개요
- 목표: Firebase 없이 Google Cloud Console만으로 iOS/Android에 구글 로그인을 붙이고,
클라이언트가 받은 Google ID Token(JWT) 을 Spring 백엔드에서 검증한 뒤 자체 JWT(Access/Refresh) 를 발급. - 핵심 포인트
- iOS/Android 클라이언트 ID는 “앱 신원” 확인용.
- Web Client ID는 ID Token 발급을 위해 Android에서 반드시 필요 (Flutter serverClientId 옵션).
- 백엔드는 여러 Client ID(iOS/Android/Web)를 허용하도록 검증해야 플랫폼별 토큰을 모두 수용 가능.
1) Google Cloud Console에서 준비하기
1-1. 프로젝트/동의 화면
- [Google Cloud Console] → 프로젝트 선택/생성
- APIs & Services → OAuth consent screen
- 앱 퍼블리싱 타입 선택(보통 “External”)
- 테스트 사용자(본인 Gmail) 추가
1-2. OAuth 2.0 Client ID 발급 (총 3개 권장)
APIs & Services → Credentials → Create Credentials → OAuth client ID
- iOS
- Application type: iOS
- Bundle ID: com.your.bundle.id
- 생성 후 iOS Client ID 확보
- Android
- Application type: Android
- Package name: com.your.package
- SHA-1: 개발/배포 환경별로 등록
- Windows (디버그 키):
- keytool -list -v -alias androiddebugkey -keystore "C:\Users\<USER>\.android\debug.keystore" -storepass android -keypass android
- macOS/Linux (디버그 키):
- keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android
- 릴리스 키:
- keytool -list -v -alias <your_alias> -keystore <path>/release.keystore
- 생성 후 Android Client ID 확보
- Web (중요)
- Application type: Web application
- 생성 후 Web Client ID 확보
- 👉 Android에서 ID Token 받으려면 Flutter에 이 값을 serverClientId로 넣어야 함
보안 팁: Client ID는 공개돼도 치명적이진 않지만, Client Secret/API Key/Service Account Key는 절대 노출 금지.
2) Flutter 클라이언트 설정
2-1. 패키지 의존성
dependencies:
google_sign_in: ^6.x.x
2-2. iOS 설정
- (A) GoogleService-Info.plist 없이 쓰는 경우
- ios/Runner/Info.plist에 URL Scheme를 직접 추가하거나,
- Flutter에서 iOS 전용 clientId 를 넘겨줍니다.
- (B) Firebase를 쓰지 않더라도 URL Scheme 설정은 필요할 수 있어요.
- 일반적으로 REVERSED_CLIENT_ID(iOS Client ID를 뒤집은 형태)를 URL Schemes에 추가합니다.
- 또는 최신 google_sign_in에서는 Dart에서 clientId를 넘겨도 동작.
2-3. Android 설정
- Web Client ID를 serverClientId로 지정해야 ID Token이 옵니다. (프로필/이메일만 필요하면 생략 가능하지만, 서버 검증/내 JWT 발급하려면 필수)
import 'package:google_sign_in/google_sign_in.dart';
final _googleSignIn = GoogleSignIn(
scopes: ['email', 'profile'],
// ✅ Android에서 ID Token 받기 위해 반드시 Web Client ID 지정
serverClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com',
// (선택) iOS에서 Info.plist 없이 사용한다면:
// clientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
);
Future<void> signIn() async {
final user = await _googleSignIn.signIn();
if (user == null) return;
final auth = await user.authentication;
final idToken = auth.idToken; // ✅ 서버로 보낼 Google ID Token
final accessToken = auth.accessToken;
// 서버로 전송 → 자체 JWT(Access/Refresh) 교환
// await AuthApiService.loginWithGoogle(idToken);
}
3) Spring 백엔드: Google ID Token 검증 → 자체 JWT 발급
3-1. 검증 개념
- 클라에서 받은 ID Token을 GoogleIdTokenVerifier로 검증
- aud(Audience) 체크: iOS/Android/Web Client ID 중 하나인지
- iss(Issuer) 체크: "accounts.google.com" 또는 "https://accounts.google.com"
- exp(만료) 체크
- email_verified 체크 권장
3-2. 서비스 코드 (질문 코드 개선 버전)
package com.darong.malgage_api.service.auth;
import com.darong.malgage_api.domain.auth.RefreshToken;
import com.darong.malgage_api.domain.user.*;
import com.darong.malgage_api.controller.dto.response.auth.TokenResponse;
import com.darong.malgage_api.repository.auth.RefreshTokenRepository;
import com.darong.malgage_api.domain.user.repository.UserRepository;
import com.darong.malgage_api.global.jwt.JwtProvider;
import com.google.api.client.googleapis.auth.oauth2.*;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class GoogleLoginService {
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final JwtProvider jwtProvider;
// ✅ 허용할 Client ID 목록 (iOS / Android / Web)
private static final List CLIENT_IDS = Arrays.asList(
"~.apps.googleusercontent.com", // iOS
"~.apps.googleusercontent.com", // Android
"~.googleusercontent.com" // Web
);
private static final List ISSUERS = Arrays.asList(
"https://accounts.google.com", "accounts.google.com"
);
@Transactional
public TokenResponse login(String idTokenString) {
GoogleIdToken.Payload payload = verifyIdToken(idTokenString);
String oauthId = payload.getSubject();
String email = payload.getEmail();
String nickname = (String) payload.get("name");
String profileImage = (String) payload.get("picture");
// email_verified 권장 체크
Object emailVerifiedObj = payload.get("email_verified");
boolean emailVerified = emailVerifiedObj instanceof Boolean && (Boolean) emailVerifiedObj;
if (!emailVerified) {
throw new IllegalArgumentException("Google email not verified");
}
User user = userRepository.findByOauthIdAndProvider(oauthId, AuthProvider.GOOGLE)
.orElseGet(() -> userRepository.save(
User.create(oauthId, AuthProvider.GOOGLE, email, nickname, profileImage)
));
// Access + Refresh Token 발급
String accessToken = jwtProvider.createAccessToken(email, AuthProvider.GOOGLE, oauthId);
String refreshToken = jwtProvider.createRefreshToken();
refreshTokenRepository.findByUserId(user.getId())
.ifPresentOrElse(
r -> r.updateToken(refreshToken),
() -> refreshTokenRepository.save(new RefreshToken(user.getId(), refreshToken))
);
return new TokenResponse(accessToken, refreshToken);
}
private GoogleIdToken.Payload verifyIdToken(String idTokenString) {
try {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(
new NetHttpTransport(),
GsonFactory.getDefaultInstance()
)
.setAudience(CLIENT_IDS) // ✅ 여러 Client ID 허용
.setIssuers(ISSUERS) // ✅ 허용 Issuer
.build();
GoogleIdToken idToken = verifier.verify(idTokenString);
if (idToken == null) {
throw new IllegalArgumentException("유효하지 않은 Google ID Token");
}
return idToken.getPayload();
} catch (Exception e) {
// 필요 시 로깅/에러 코드 구분
throw new RuntimeException("구글 토큰 검증 실패", e);
}
}
}
운영/개발 분리 팁: CLIENT_IDS, ISSUERS를 환경변수/설정으로 분리하면 깔끔합니다.
예: application.yml에 리스트로 주입하고 @ConfigurationProperties로 바인딩.
google:
oauth:
client-ids:
- ${GOOGLE_CLIENT_ID_IOS}
- ${GOOGLE_CLIENT_ID_ANDROID}
- ${GOOGLE_CLIENT_ID_WEB}
4) 전체 로그인 플로우
- Flutter에서 Google 로그인 → GoogleSignInAccount 획득
- account.authentication 으로 ID Token 획득
- Android: serverClientId(=Web Client ID) 지정 필수
- ID Token을 백엔드 /auth/google 로 전송
- 서버에서 ID Token 검증(aud/iss/exp/email_verified)
- 사용자 생성/조회 → 내 JWT(Access/Refresh) 발급 → 응답
- 클라이언트는 이후 API 호출 시 Access Token 사용
마무리
- iOS/Android 모두 Cloud Console만으로 구글 로그인을 붙일 수 있고,
- Android에서 ID Token을 받으려면 Web Client ID가 반드시 필요합니다.
- 백엔드는 여러 Client ID 허용 → 검증 → 자체 JWT 발급 구조가 가장 안정적입니다.
'Front-End > Flutter' 카테고리의 다른 글
[Flutter] qr_code_scanner 소개 및 사용방법 (0) | 2024.04.12 |
---|---|
[Flutter] 플러터 InAppWebView JS로 값 주고받기 (0) | 2024.02.22 |
[Flutter] 플러터 InAppWebView Post로 데이터 전송하기 (0) | 2024.02.22 |