Google快捷登录配置 在很多网站中,我们都会看到“使用 Google 账号登录”按钮。用户不需要重新注册账号,只需要授权 Google 登录,网站就可以拿到用户的基础身份信息,然后完成本系统的注册、绑定或登录。
本文记录一套完整的 Google 快捷登录配置流程,从 Google Cloud Console 创建项目、获取 Client ID,到前端接入登录按钮、后端校验 ID Token,再到实际项目中如何落库和生成自己系统的登录态。
本文主要讲的是“使用 Google 账号登录网站”,不是调用 Gmail、Google Drive、Google Calendar 等 Google API。如果只是做快捷登录,通常只需要 ID Token,不需要长期保存 Google Access Token 或 Refresh Token。
一、整体登录流程 Google 快捷登录的核心流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 用户点击“使用 Google 登录” ↓ Google 弹出登录/授权窗口 ↓ Google 返回 credential,也就是 ID Token ↓ 前端把 ID Token 发给后端 ↓ 后端校验 ID Token 的真实性 ↓ 后端根据 Google 用户唯一 ID 查询或创建本地用户 ↓ 后端生成自己系统的登录态,比如 Session、JWT、Sa-Token ↓ 用户登录成功
这里最重要的一点是:
1 2 Google 只负责证明“这个 Google 用户是谁” 你自己的系统负责决定“这个用户在我系统里是谁、有什么权限、登录态怎么维护”
所以,不能直接把 Google 返回的用户信息当成本系统登录态。前端拿到 Google 返回的 credential 后,必须交给后端校验,后端校验通过后,再生成自己系统的 token。
二、几个核心概念 在正式配置之前,先明确几个概念。
1. Client ID Client ID 是 Google 分配给你的网站应用的公开标识,格式大致如下:
1 1234567890-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
前端初始化 Google 登录组件时需要使用它,后端校验 ID Token 时也需要使用它。
2. Client Secret Client Secret 是客户端密钥,不能放在前端代码中。
如果只是使用 Google Identity Services 做普通网站登录,前端通过 Google 登录按钮拿到 ID Token,再交给后端校验,一般不需要在前端使用 Client Secret。
3. ID Token ID Token 是 Google 返回的 JWT,里面包含用户身份信息,例如:
1 2 3 4 5 6 7 8 9 10 { "sub" : "Google用户唯一ID" , "email" : "用户邮箱" , "email_verified" : true , "name" : "用户名称" , "picture" : "头像地址" , "iss" : "https://accounts.google.com" , "aud" : "你的ClientID" , "exp" : 过期时间 }
后端要校验这个 token 是否真的由 Google 签发、是否过期、是否发给你的应用。
4. sub sub 是 Google 用户唯一 ID。实际项目中,第三方账号绑定表应该保存 sub,不要把 email 当成唯一标识。
因为邮箱可能变化,但 sub 更适合作为 Google 账号在你系统中的唯一绑定标识。
三、在 Google Cloud Console 创建项目 1. 进入 Google Cloud Console 打开 Google Cloud Console,登录你的 Google 账号。
进入后,在顶部项目选择器中点击项目名称,然后点击“新建项目”。
2. 创建新项目 填写项目信息:
1 2 3 Project name:你的项目名称,例如 yao-admin Organization:个人账号可以不选 Location:默认即可
点击创建后,等待项目创建完成。
创建完成后,记得切换到刚刚创建的项目。
3. 进入 API 和服务 在左侧菜单中找到:
1 API 和服务 / APIs & Services
后续 OAuth 同意屏幕和凭据都在这里配置。
如果 Google Cloud Console 页面菜单有变化,可以直接在顶部搜索框搜索:
1 2 3 OAuth consent screen Credentials Clients
四、配置 OAuth 同意屏幕 Google 登录时,用户会看到一个授权页面,上面显示应用名称、开发者邮箱、应用域名等信息。这个页面就是 OAuth 同意屏幕。
1. 打开 OAuth 同意屏幕 进入:
或者在新版本控制台中找到类似:
1 Google Auth Platform → Branding
不同账号和控制台版本看到的菜单名称可能略有差异,但核心配置项是一样的。
2. 选择用户类型 如果是个人网站或普通公开网站,通常选择:
含义是允许任意 Google 账号登录。
如果你的账号属于 Google Workspace,并且只允许组织内部账号登录,可以选择:
3. 填写应用信息 常见必填项包括:
1 2 3 App name:应用名称,例如 Yao Admin User support email:用户支持邮箱 Developer contact information:开发者联系邮箱
如果是正式上线项目,建议补充:
1 2 3 4 Application home page:应用首页 Application privacy policy link:隐私政策地址 Application terms of service link:服务条款地址 Authorized domains:授权域名
例如你的站点是:
那么授权域名可以填写:
注意:授权域名一般只填根域名,不需要带 https://。
4. Scopes 权限范围 如果只是做快捷登录,通常使用默认的基础信息即可,例如:
这些权限用于获取用户身份、邮箱和基础资料。
不要一开始就申请 Gmail、Drive 等敏感权限。权限越多,审核要求越高,用户看到的授权提示也越敏感。
5. 测试用户 如果应用还处于测试模式,可能需要添加测试用户。
例如添加你自己的 Google 邮箱:
测试阶段,只有测试用户可以正常完成授权。
正式上线后,可以发布应用,让所有用户使用。
五、创建 OAuth 2.0 Client ID OAuth 同意屏幕配置完成后,就可以创建 Client ID。
1. 打开凭据页面 进入:
1 API 和服务 → 凭据 / Credentials
点击:
2. 选择应用类型 应用类型选择:
也就是 Web 应用。
3. 填写名称 名称可以随便写,建议能区分环境:
1 2 Yao Admin Web Client Dev Yao Admin Web Client Prod
实际项目中,开发环境和生产环境最好分开创建两个 Client ID,避免本地调试域名和生产域名混在一起。
4. 配置 Authorized JavaScript origins 这里填写允许使用这个 Client ID 的前端站点来源。
开发环境常见配置:
1 2 3 http://localhost:3000 http://localhost:5173 http://127.0.0.1:5173
生产环境示例:
1 2 https://yao-admin.com https://www.yao-admin.com
注意事项:
1 2 3 4 1. 必须包含协议,例如 http:// 或 https:// 2. 不要填写路径,例如不要写 https://yao-admin.com/login 3. 不同端口算不同 origin 4. 生产环境必须使用 HTTPS
5. 配置 Authorized redirect URIs 如果你使用的是前端 callback 模式,也就是前端拿到 Google 返回的 credential 后再请求后端接口,那么通常不一定要配置 redirect URI。
如果你使用的是 redirect 模式,让 Google 直接把 credential POST 到后端登录接口,则需要配置后端地址,例如:
1 https://yao-admin.com/api/auth/google/login
普通前后端分离项目中,推荐先使用前端 callback 模式,流程更直观。
6. 获取 Client ID 创建完成后,Google 会生成:
前端只使用 Client ID。
后端校验 ID Token 时也需要 Client ID。
Client Secret 不要提交到 Git,不要放到前端环境变量中,也不要写死在 JS 代码里。
六、前端接入 Google 登录按钮 下面以前端页面直接接入 Google Identity Services 为例。
1. 引入 Google Identity Services 脚本 在 HTML 中引入:
1 <script src ="https://accounts.google.com/gsi/client" async defer > </script >
如果是 Vite、React、Vue 项目,可以放在 index.html 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 <!doctype html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <script src ="https://accounts.google.com/gsi/client" async defer > </script > <title > Yao Admin</title > </head > <body > <div id ="root" > </div > <script type ="module" src ="/src/main.tsx" > </script > </body > </html >
2. 配置前端环境变量 以 Vite 项目为例,在 .env.development 中配置:
1 VITE_GOOGLE_CLIENT_ID=你的GoogleClientID
生产环境 .env.production:
1 VITE_GOOGLE_CLIENT_ID=生产环境GoogleClientID
注意:
1 2 VITE_GOOGLE_CLIENT_ID 可以暴露给前端 Client Secret 不可以暴露给前端
3. React 示例代码 下面是一个简单的 React 登录按钮组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import { useEffect, useRef } from 'react' ;interface GoogleCredentialResponse { credential : string ; select_by?: string ; } export default function GoogleLoginButton ( ) { const buttonRef = useRef<HTMLDivElement | null >(null ); useEffect (() => { const clientId = import .meta .env .VITE_GOOGLE_CLIENT_ID ; if (!clientId) { console .error ('VITE_GOOGLE_CLIENT_ID 未配置' ); return ; } if (!window .google || !buttonRef.current ) { console .error ('Google Identity Services 脚本未加载完成' ); return ; } window .google .accounts .id .initialize ({ client_id : clientId, callback : async (response : GoogleCredentialResponse ) => { await handleGoogleLogin (response.credential ); } }); window .google .accounts .id .renderButton (buttonRef.current , { theme : 'outline' , size : 'large' , text : 'signin_with' , shape : 'rectangular' , width : 260 }); }, []); async function handleGoogleLogin (credential: string ) { const res = await fetch ('/api/auth/google/login' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify ({ credential }) }); if (!res.ok ) { throw new Error ('Google 登录失败' ); } const data = await res.json (); localStorage .setItem ('token' , data.token ); window .location .href = '/' ; } return <div ref ={buttonRef} > </div > ; }
如果 TypeScript 提示 window.google 不存在,可以增加类型声明。
在 src/types/google.d.ts 中添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export {};declare global { interface Window { google?: { accounts : { id : { initialize : (config: { client_id: string ; callback: (response: { credential: string ; select_by?: string }) => void ; } ) => void ; renderButton : ( parent: HTMLElement, options: { theme?: 'outline' | 'filled_blue' | 'filled_black' ; size?: 'large' | 'medium' | 'small' ; text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin' ; shape?: 'rectangular' | 'pill' | 'circle' | 'square' ; width?: number ; } ) => void ; disableAutoSelect : () => void ; }; }; }; } }
4. Vue 示例代码 如果是 Vue3 项目,也可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <template> <div ref="googleButtonRef"></div> </template> <script setup lang="ts"> import { onMounted, ref } from 'vue'; const googleButtonRef = ref<HTMLElement | null>(null); onMounted(() => { const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID; if (!clientId) { console.error('VITE_GOOGLE_CLIENT_ID 未配置'); return; } if (!window.google || !googleButtonRef.value) { console.error('Google Identity Services 脚本未加载完成'); return; } window.google.accounts.id.initialize({ client_id: clientId, callback: async (response: { credential: string }) => { await handleGoogleLogin(response.credential); } }); window.google.accounts.id.renderButton(googleButtonRef.value, { theme: 'outline', size: 'large', text: 'signin_with', width: 260 }); }); async function handleGoogleLogin(credential: string) { const res = await fetch('/api/auth/google/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential }) }); if (!res.ok) { throw new Error('Google 登录失败'); } const data = await res.json(); localStorage.setItem('token', data.token); window.location.href = '/'; } </script>
七、后端接收 Google ID Token 前端把 credential 传给后端后,后端要做的是:
1 2 3 4 5 6 1. 接收 credential 2. 校验 ID Token 3. 解析 Google 用户信息 4. 查询或创建本地用户 5. 生成本系统登录 token 6. 返回给前端
下面以 Spring Boot 为例。
1. 添加依赖 在 pom.xml 中添加 Google API Client 依赖:
1 2 3 4 5 <dependency > <groupId > com.google.api-client</groupId > <artifactId > google-api-client</artifactId > <version > 2.7.2</version > </dependency >
如果你的项目版本较新,也可以根据 Maven 仓库中的最新版本调整。
2. 配置 application.yml 1 2 3 google: oauth: client-id: ${GOOGLE_CLIENT_ID}
生产环境中建议通过环境变量注入:
1 export GOOGLE_CLIENT_ID="你的GoogleClientID"
不要把敏感配置直接写死到代码仓库里。
3. 请求对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import jakarta.validation.constraints.NotBlank;public class GoogleLoginRequest { @NotBlank(message = "credential不能为空") private String credential; public String getCredential () { return credential; } public void setCredential (String credential) { this .credential = credential; } }
4. 返回对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class LoginResponse { private String token; private UserInfo userInfo; public LoginResponse (String token, UserInfo userInfo) { this .token = token; this .userInfo = userInfo; } public String getToken () { return token; } public UserInfo getUserInfo () { return userInfo; } public static class UserInfo { private Long id; private String nickname; private String email; private String avatar; public UserInfo (Long id, String nickname, String email, String avatar) { this .id = id; this .nickname = nickname; this .email = email; this .avatar = avatar; } public Long getId () { return id; } public String getNickname () { return nickname; } public String getEmail () { return email; } public String getAvatar () { return avatar; } } }
5. Controller 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import jakarta.validation.Valid;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/api/auth/google") public class GoogleAuthController { private final GoogleAuthService googleAuthService; public GoogleAuthController (GoogleAuthService googleAuthService) { this .googleAuthService = googleAuthService; } @PostMapping("/login") public LoginResponse login (@Valid @RequestBody GoogleLoginRequest request) { return googleAuthService.login(request.getCredential()); } }
6. 校验 ID Token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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.gson.GsonFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.util.Collections;@Service public class GoogleTokenVerifierService { private final String googleClientId; public GoogleTokenVerifierService (@Value("${google.oauth.client-id}") String googleClientId) { this .googleClientId = googleClientId; } public GoogleUserInfo verify (String credential) { try { GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier .Builder( new NetHttpTransport (), GsonFactory.getDefaultInstance() ) .setAudience(Collections.singletonList(googleClientId)) .build(); GoogleIdToken idToken = verifier.verify(credential); if (idToken == null ) { throw new RuntimeException ("无效的Google登录凭证" ); } GoogleIdToken.Payload payload = idToken.getPayload(); String googleUserId = payload.getSubject(); String email = payload.getEmail(); Boolean emailVerified = payload.getEmailVerified(); String name = (String) payload.get("name" ); String picture = (String) payload.get("picture" ); String locale = (String) payload.get("locale" ); return new GoogleUserInfo ( googleUserId, email, Boolean.TRUE.equals(emailVerified), name, picture, locale ); } catch (Exception e) { throw new RuntimeException ("Google ID Token校验失败" , e); } } }
7. Google 用户信息对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class GoogleUserInfo { private final String googleUserId; private final String email; private final boolean emailVerified; private final String name; private final String picture; private final String locale; public GoogleUserInfo (String googleUserId, String email, boolean emailVerified, String name, String picture, String locale) { this .googleUserId = googleUserId; this .email = email; this .emailVerified = emailVerified; this .name = name; this .picture = picture; this .locale = locale; } public String getGoogleUserId () { return googleUserId; } public String getEmail () { return email; } public boolean isEmailVerified () { return emailVerified; } public String getName () { return name; } public String getPicture () { return picture; } public String getLocale () { return locale; } }
八、数据库表设计 实际项目中,建议把本地用户表和第三方账号绑定表拆开。
1. 用户表 1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR (100 ) DEFAULT NULL COMMENT '用户名' , nickname VARCHAR (100 ) DEFAULT NULL COMMENT '昵称' , email VARCHAR (255 ) DEFAULT NULL COMMENT '邮箱' , avatar VARCHAR (500 ) DEFAULT NULL COMMENT '头像' , status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常,0禁用' , create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , UNIQUE KEY uk_email (email) ) COMMENT= '用户表' ;
2. 第三方账号绑定表 1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE sys_oauth_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '本地用户ID' , provider VARCHAR (50 ) NOT NULL COMMENT '第三方平台,例如 google、github、wechat' , provider_user_id VARCHAR (255 ) NOT NULL COMMENT '第三方平台用户唯一ID' , email VARCHAR (255 ) DEFAULT NULL COMMENT '第三方账号邮箱' , nickname VARCHAR (100 ) DEFAULT NULL COMMENT '第三方账号昵称' , avatar VARCHAR (500 ) DEFAULT NULL COMMENT '第三方账号头像' , create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , UNIQUE KEY uk_provider_user (provider, provider_user_id), KEY idx_user_id (user_id) ) COMMENT= '第三方账号绑定表' ;
其中 Google 登录时:
1 2 provider = google provider_user_id = Google ID Token 中的 sub
九、登录业务逻辑 后端拿到 Google 用户信息后,可以按下面逻辑处理:
1 2 3 4 5 6 1. 根据 provider = google 和 provider_user_id = sub 查询 sys_oauth_account 2. 如果查到了,说明之前登录过,直接找到本地用户 3. 如果没查到,根据业务规则判断是否创建新用户或绑定已有用户 4. 创建或更新用户信息 5. 生成本系统登录态 6. 返回 token 和用户信息
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import cn.dev33.satoken.stp.StpUtil;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service public class GoogleAuthService { private static final String PROVIDER_GOOGLE = "google" ; private final GoogleTokenVerifierService googleTokenVerifierService; private final UserRepository userRepository; private final OAuthAccountRepository oAuthAccountRepository; public GoogleAuthService (GoogleTokenVerifierService googleTokenVerifierService, UserRepository userRepository, OAuthAccountRepository oAuthAccountRepository) { this .googleTokenVerifierService = googleTokenVerifierService; this .userRepository = userRepository; this .oAuthAccountRepository = oAuthAccountRepository; } @Transactional(rollbackFor = Exception.class) public LoginResponse login (String credential) { GoogleUserInfo googleUserInfo = googleTokenVerifierService.verify(credential); OAuthAccount oauthAccount = oAuthAccountRepository.findByProviderAndProviderUserId( PROVIDER_GOOGLE, googleUserInfo.getGoogleUserId() ); User user; if (oauthAccount != null ) { user = userRepository.findById(oauthAccount.getUserId()); if (user == null ) { throw new RuntimeException ("绑定的本地用户不存在" ); } } else { user = createOrBindUser(googleUserInfo); OAuthAccount newOAuthAccount = new OAuthAccount (); newOAuthAccount.setUserId(user.getId()); newOAuthAccount.setProvider(PROVIDER_GOOGLE); newOAuthAccount.setProviderUserId(googleUserInfo.getGoogleUserId()); newOAuthAccount.setEmail(googleUserInfo.getEmail()); newOAuthAccount.setNickname(googleUserInfo.getName()); newOAuthAccount.setAvatar(googleUserInfo.getPicture()); oAuthAccountRepository.save(newOAuthAccount); } if (user.getStatus() == 0 ) { throw new RuntimeException ("当前用户已被禁用" ); } StpUtil.login(user.getId()); String token = StpUtil.getTokenValue(); return new LoginResponse ( token, new LoginResponse .UserInfo( user.getId(), user.getNickname(), user.getEmail(), user.getAvatar() ) ); } private User createOrBindUser (GoogleUserInfo googleUserInfo) { User user = null ; if (googleUserInfo.isEmailVerified()) { user = userRepository.findByEmail(googleUserInfo.getEmail()); } if (user != null ) { return user; } User newUser = new User (); newUser.setEmail(googleUserInfo.getEmail()); newUser.setNickname(googleUserInfo.getName()); newUser.setAvatar(googleUserInfo.getPicture()); newUser.setStatus(1 ); userRepository.save(newUser); return newUser; } }
这里的 UserRepository 和 OAuthAccountRepository 可以是 MyBatis、MyBatis-Plus、JPA 或你自己的 DAO。
核心不是 Repository 怎么写,而是流程要清楚:
1 2 3 先校验 Google ID Token 再用 Google sub 查绑定关系 再生成自己系统的登录 token
十、前后端联调 1. 本地启动前端 假设前端端口是 5173,则 Google Cloud Console 的 Authorized JavaScript origins 中必须有:
如果你使用的是 127.0.0.1,也要单独配置:
2. 本地启动后端 假设后端端口是 8080,前端通过代理请求后端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { defineConfig } from 'vite' ;import react from '@vitejs/plugin-react' ;export default defineConfig ({ plugins : [react ()], server : { proxy : { '/api' : { target : 'http://localhost:8080' , changeOrigin : true } } } });
这样前端请求:
会被代理到:
1 http://localhost:8080/api/auth/google/login
3. 测试登录流程 浏览器打开登录页,点击 Google 登录按钮。
成功后,前端控制台应该能看到后端返回的数据,例如:
1 2 3 4 5 6 7 8 9 { "token" : "xxxxxx" , "userInfo" : { "id" : 10001 , "nickname" : "Yao" , "email" : "yao@example.com" , "avatar" : "https://..." } }
然后前端保存 token,并跳转到首页。
十一、生产环境部署注意事项 1. 必须配置生产域名 生产环境的前端域名必须加入 Authorized JavaScript origins,例如:
如果你同时支持 www 域名,也要加:
1 https://www.yao-admin.com
2. 建议使用 HTTPS 生产环境必须使用 HTTPS,否则 Google 登录、Cookie 安全策略、浏览器跨域策略都可能出现问题。
3. Nginx 反向代理示例 如果前端和后端都挂在同一个域名下,可以用 Nginx 这样配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 server { listen 443 ssl http2; server_name yao-admin.com; ssl_certificate /etc/letsencrypt/live/yao-admin.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yao-admin.com/privkey.pem; root /var/www/yao-admin/dist; index index.html; location / { try_files $uri $uri / /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; proxy_set_header X-Forwarded-Proto $scheme ; } }
这种方式的好处是:
1 2 前端地址:https://yao-admin.com 后端接口:https://yao-admin.com/api/xxx
浏览器不会遇到跨域问题。
4. Cookie 登录态配置 如果你使用 Cookie 保存登录态,建议配置:
1 2 3 HttpOnly Secure SameSite=Lax 或 SameSite=None
如果前后端不同域名,例如:
1 2 前端:https://www.example.com 后端:https://api.example.com
并且需要跨站 Cookie,那么通常需要:
同时后端 CORS 要允许携带凭证。
十二、常见问题 1. 点击 Google 登录后提示 origin 不匹配 通常是 Authorized JavaScript origins 没有配置当前前端地址。
检查:
1 2 3 4 1. 协议是否一致:http / https 2. 域名是否一致:localhost / 127.0.0.1 / 正式域名 3. 端口是否一致:3000 / 5173 / 其他端口 4. 是否误填了路径
例如前端是:
1 http://localhost:5173/login
Google Cloud Console 中应该填:
而不是:
1 http://localhost:5173/login
2. 后端校验 token 失败 常见原因:
1 2 3 4 5 1. 后端配置的 Client ID 和前端使用的 Client ID 不一致 2. credential 传错了,传成了 access_token 或其他字段 3. token 已经过期 4. 本地时间不准确 5. 使用了其他项目下的 Client ID
后端校验时,aud 必须等于你的 Web Client ID。
3. 前端 window.google 是 undefined 可能原因:
1 2 3 4 1. Google 脚本还没加载完成 2. index.html 没有引入 https://accounts.google.com/gsi/client 3. 网络无法访问 Google 资源 4. 浏览器插件拦截了 Google 登录脚本
可以先在浏览器控制台检查:
4. 是否需要 Client Secret 如果只是前端接入 Google 登录按钮,然后把 ID Token 发给后端校验,一般不需要在前端使用 Client Secret。
Client Secret 不能放到前端。
如果后续需要服务端授权码流程、访问 Google API、换取 access token 或 refresh token,才会涉及 Client Secret。
5. 是否可以只在前端解析 ID Token 不建议。
前端解析 JWT 只能看到内容,不能作为可信登录依据。真正的登录判断必须在后端完成,后端需要校验签名、aud、iss、exp 等字段。
6. 是否可以用 email 作为唯一标识 不建议。
Google 用户唯一标识应该使用 ID Token 里的 sub 字段。
邮箱可以作为展示信息或辅助绑定依据,但不应该作为第三方账号唯一主键。
7. 同一个邮箱已经注册过账号怎么办 常见处理方式有两种:
1 2 方式一:如果 email_verified = true,自动绑定已有邮箱账号 方式二:要求用户先登录原账号,再到账号设置中绑定 Google
从安全角度看,方式二更稳妥。
如果是个人项目或内部项目,方式一实现更简单。
十三、最终落地清单 Google Cloud Console 中需要完成:
1 2 3 4 5 6 1. 创建 Google Cloud 项目 2. 配置 OAuth 同意屏幕 3. 创建 OAuth 2.0 Client ID 4. 应用类型选择 Web application 5. 配置 Authorized JavaScript origins 6. 复制 Client ID
前端需要完成:
1 2 3 4 5 6 1. 引入 https://accounts.google.com/gsi/client 2. 配置 VITE_GOOGLE_CLIENT_ID 3. 渲染 Google 登录按钮 4. 拿到 response.credential 5. 调用后端 /api/auth/google/login 6. 保存后端返回的本系统 token
后端需要完成:
1 2 3 4 5 6 7 8 1. 接收 credential 2. 使用 Google API Client 校验 ID Token 3. 校验 aud、iss、exp 4. 获取 sub、email、name、picture 5. 根据 sub 查询第三方账号绑定表 6. 查询或创建本地用户 7. 生成自己系统的登录态 8. 返回 token 和用户信息
数据库建议准备:
1 2 1. sys_user 用户表 2. sys_oauth_account 第三方账号绑定表
十四、总结 Google 快捷登录并不复杂,核心就是三步:
1 2 3 1. 在 Google Cloud Console 创建 Web Client ID 2. 前端通过 Google Identity Services 获取 ID Token 3. 后端校验 ID Token 后,生成自己系统的登录态
实际开发时,最容易出错的是:
1 2 3 4 5 1. Authorized JavaScript origins 配置不对 2. 前后端使用的 Client ID 不一致 3. 后端没有校验 ID Token,直接相信前端数据 4. 错误地使用 email 作为唯一标识 5. 把 Client Secret 放到了前端
只要把这些点处理好,Google 快捷登录就可以稳定接入到实际项目中。