You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter移动端对接Spring Boot OAuth2邮箱登录方案咨询

Flutter 集成 Spring Boot OAuth2 Google 邮箱登录方案

下面提供两种适配你现有后端OAuth2配置的实现方案,按需选择:

方案一:复用后端OAuth2授权页面(WebView方式)

这种方案直接借助后端已有的/oauth2/authorization/google授权流程,通过WebView在移动端完成登录,无需大幅修改后端配置。

1. 配置后端回调地址

在Spring Boot的OAuth2客户端配置中,添加移动端自定义Scheme的回调地址,比如myapp://oauth2callback:

@Configuration
public class OAuth2Config {
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
                .clientId("your-google-client-id")
                .clientSecret("your-google-client-secret")
                .redirectUri("{baseUrl}/login/oauth2/code/google,myapp://oauth2callback") // 添加移动端回调
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .scope("email", "profile", "openid")
                .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
                .tokenUri("https://www.googleapis.com/oauth2/v4/token")
                .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
                .userNameAttributeName(IdTokenClaimNames.SUB)
                .clientName("Google")
                .build();
    }
}

2. Flutter端实现WebView登录流程

首先添加webview_flutter依赖到pubspec.yaml:

dependencies:
  webview_flutter: ^4.4.0
  http: ^0.13.5
  shared_preferences: ^2.2.2

然后编写登录页面,加载后端授权URL并监听回调:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class OAuthLoginPage extends StatefulWidget {
  final String authUrl = "https://your-backend-domain/oauth2/authorization/google";
  final String redirectUri = "myapp://oauth2callback";

  const OAuthLoginPage({super.key});

  @override
  State<OAuthLoginPage> createState() => _OAuthLoginPageState();
}

class _OAuthLoginPageState extends State<OAuthLoginPage> {
  late WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse(widget.authUrl))
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: (NavigationRequest request) {
            if (request.url.startsWith(widget.redirectUri)) {
              // 提取授权码
              final code = Uri.parse(request.url).queryParameters['code'];
              if (code != null) {
                _exchangeToken(code);
              }
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      );
  }

  Future<void> _exchangeToken(String code) async {
    try {
      final response = await http.post(
        Uri.parse("https://your-backend-domain/oauth2/token"),
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: {
          'grant_type': 'authorization_code',
          'code': code,
          'redirect_uri': widget.redirectUri,
          'client_id': 'your-backend-client-id',
          'client_secret': 'your-backend-client-secret',
        },
      );

      if (response.statusCode == 200) {
        final tokenData = jsonDecode(response.body);
        final accessToken = tokenData['access_token'];
        final refreshToken = tokenData['refresh_token'];

        // 保存token到本地
        final prefs = await SharedPreferences.getInstance();
        await prefs.setString('access_token', accessToken);
        await prefs.setString('refresh_token', refreshToken);

        // 返回首页或登录成功页面
        if (mounted) {
          Navigator.pop(context);
        }
      } else {
        throw Exception('Token交换失败');
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('登录失败: ${e.toString()}')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Google邮箱登录')),
      body: WebViewWidget(controller: controller),
    );
  }
}

3. 配置移动端自定义Scheme

Android

在android/app/src/main/AndroidManifest.xml的<activity>标签内添加:

<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="myapp" android:host="oauth2callback" />
</intent-filter>

iOS

在ios/Runner/Info.plist中添加:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

方案二:使用Google原生登录SDK(体验更优)

这种方案借助Flutter的google_sign_in插件完成原生登录,将Google返回的id_token传递给后端验证,后端再颁发自有token。

1. Flutter端集成Google登录

添加依赖到pubspec.yaml:

dependencies:
  google_sign_in: ^5.4.0
  http: ^0.13.5
  shared_preferences: ^2.2.2

实现登录逻辑:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

final GoogleSignIn _googleSignIn = GoogleSignIn(
  scopes: ['email', 'openid', 'profile'],
);

Future<void> signInWithGoogle(BuildContext context) async {
  try {
    final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
    if (googleUser == null) return;

    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
    final idToken = googleAuth.idToken;

    // 调用后端验证id_token并获取自有token
    final response = await http.post(
      Uri.parse("https://your-backend-domain/api/auth/google"),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'idToken': idToken}),
    );

    if (response.statusCode == 200) {
      final tokenData = jsonDecode(response.body);
      final accessToken = tokenData['access_token'];

      // 保存token
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('access_token', accessToken);

      // 跳转登录成功页面
      if (context.mounted) {
        Navigator.pushReplacementNamed(context, '/home');
      }
    } else {
      throw Exception('后端验证失败');
    }
  } catch (e) {
    if (context.mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('登录失败: ${e.toString()}')),
      );
    }
  }
}

2. Spring Boot后端验证id_token

添加Google验证依赖到pom.xml:

<dependency>
    <groupId>com.google.auth</groupId>
    <artifactId>google-auth-library-oauth2-http</artifactId>
    <version>1.19.0</version>
</dependency>

编写验证接口:

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.Map;

@RestController
public class GoogleAuthController {

    private static final String GOOGLE_CLIENT_ID = "your-google-mobile-client-id";

    @PostMapping("/api/auth/google")
    public ResponseEntity<?> authenticateGoogleUser(@RequestBody Map<String, String> request) {
        String idToken = request.get("idToken");

        try {
            GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance())
                    .setAudience(Collections.singletonList(GOOGLE_CLIENT_ID))
                    .build();

            GoogleIdToken idTokenObj = verifier.verify(idToken);
            if (idTokenObj != null) {
                GoogleIdToken.Payload payload = idTokenObj.getPayload();
                String email = payload.getEmail();
                boolean emailVerified = payload.getEmailVerified();

                if (!emailVerified) {
                    return ResponseEntity.badRequest().body("邮箱未验证");
                }

                // 根据email查找/创建用户,生成并返回自有token(可使用Spring Security OAuth2或JWT)
                String accessToken = generateUserToken(email);
                return ResponseEntity.ok(Map.of("access_token", accessToken));
            } else {
                return ResponseEntity.badRequest().body("无效的ID Token");
            }
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("Token验证失败: " + e.getMessage());
        }
    }

    // 自定义生成token的方法,根据你的后端实现调整
    private String generateUserToken(String email) {
        // 示例:生成JWT或调用OAuth2 token服务
        return "your-generated-access-token";
    }
}

方案对比

方案优点缺点
WebView方式完全复用后端现有OAuth2配置登录体验不如原生SDK流畅
原生SDK方式原生登录体验,速度快需要后端新增id_token验证逻辑

注意事项

  • 确保Google开发者控制台中,移动端和后端的客户端ID配置正确,且启用了Google OAuth2服务。
  • 敏感信息(如client_secret)不要硬编码在Flutter代码中,可通过后端配置或环境变量管理。
  • 存储token时建议使用加密存储(如encrypted_shared_preferences),避免明文泄露。
  • 处理token过期逻辑,使用refresh_token刷新access_token(WebView方案可直接复用后端OAuth2刷新流程)。

内容的提问来源于stack exchange,提问作者DangTri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 07:29:52