Google 登录
本文档详细描述了如何实现 Google 登录功能,包括前置准备、整体登录流程、后端接口及代码示例。通过本文档,您可以了解如何配置 Google 开发者平台、前后端交互细节、授权码交换、用户信息处理以及生成内部认证 token 的具体步骤。
目录
前置工作
在实现 Google 登录前,需完成以下准备工作:
获取 Google Key 和 Secret:
确保在 Google Cloud Console 上获取到 Google 的 Client ID 与 Client Secret,这两个参数将在后端用于与 Google OAuth 2.0 服务进行交互。选择您的 GCP 项目:
- 打开 Google Cloud Console 并确保选择了与 Firebase 同一个项目(通常项目名称与 Firebase 项目一致)。
进入 APIs & Services -> Credentials:
- 在左侧菜单中选择 “APIs & Services”,点击后再点击 “Credentials”(凭据)。
- 找到对应的 “OAuth 2.0 Client IDs”,通常会显示为 “Web client (Auto-created for Firebase...)” 等名称。
编辑 OAuth 客户端 ID:
- 点击选中对应的 OAuth Client ID,然后点击 “Edit OAuth Client”(编辑)。
- 在 “Authorized redirect URIs” 中,添加前端实际使用的完整回调地址。
注意: 此地址必须与请求中的redirect_uri
参数完全一致,包括协议(http/https)、域名、路径、端口及末尾斜杠。
登录流程
Google 登录不仅仅涉及前端的跳转,还需要后端参与授权码交换、用户信息处理和 token 签发。下面详细介绍各步骤。
1. 是否需要后端支持
需要后端支持。
Google 登录流程包括以下关键环节,后端必须配合完成:
授权码交换:
前端在用户授权后获取到一个临时授权码(code),然后将该授权码传递给后端。后端利用预先配置的GOOGLE_CLIENT_ID
和GOOGLE_CLIENT_SECRET
与 Google 服务器交互,交换出访问令牌(access token)及其他相关信息。用户信息处理:
后端解析 Google 返回的用户信息(如 email、用户 ID 等),并根据数据库中是否存在该用户决定是否创建新用户。Token 签发:
后端根据业务逻辑生成内部认证 token 和 refresh token,供后续接口调用时使用。
2. 前后端 Google 登录流程
整个登录流程主要分为以下步骤:
前端发起登录请求:
当用户点击 Google 登录按钮时,前端会将用户重定向至 Google 授权页面,同时传递必要的参数,如client_id
、redirect_uri
、scope
、response_type
等。例如,可能会发起如下请求:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=125062456843-b0o5sp6cm0fff067lanph5i9uf1lih3l.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Frumi-bdb43.firebaseapp.com%2F__%2Fauth%2Fhandler&state=AMbdmDkEVT5Bj0C_nXTrrL6_zmOE51bvsT_qPzj8WqXDx-XrP5yOsL4gWlO8VgWXeNws6jAhzoFjR0-eQgv4BaWcfN6HSTZoIlT_EZMvuUr-V4I_suq4_jV0RkmNg_eyYR28iaOXsJGrIoFcsN8c6DNiod8sKYVGigkU4tKaom08G1SeGJsTTHTWq0baflzsv0fxTskx8C3c8oxGEegdR_PSeOoRctXHZnf1wf6w3snuTv4lv0hyhqAR4QG1X9qa_BYdsDzvRkNqd5_9hwgEClewJN_m6Y0jEu5mjjcgedCfJNKhgfztV75V2zMamkynguqL3_U&scope=openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20profile
或
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=125062456843-b0o5sp6cm0fff067lanph5i9uf1lih3l.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fcollegebot.ai%2Fgoogle%2Flogin&state=AMbdmDkEVT5Bj0C_nXTrrL6_zmOE51bvsT_qPzj8WqXDx-XrP5yOsL4gWlO8VgWXeNws6jAhzoFjR0-eQgv4BaWcfN6HSTZoIlT_EZMvuUr-V4I_suq4_jV0RkmNg_eyYR28iaOXsJGrIoFcsN8c6DNiod8sKYVGigkU4tKaom08G1SeGJsTTHTWq0baflzsv0fxTskx8C3c8oxGEegdR_PSeOoRctXHZnf1wf6w3snuTv4lv0hyhqAR4QG1X9qa_BYdsDzvRkNqd5_9hwgEClewJN_m6Y0jEu5mjjcgedCfJNKhgfztV75V2zMamkynguqL3_U&scope=openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20profile
参数说明:
- response_type=code: 表示授权后返回授权码。
- client_id: 从 Google 开发者控制台获得的 Client ID。
- redirect_uri: 用户授权后,Google 将重定向到预先配置的地址。该地址必须与 Google 控制台中配置的完全一致。
- state: 可选参数,用于防止 CSRF 攻击。前后端传递同一随机字符串,在授权回调时原样返回,便于验证请求有效性。
- scope: 请求的权限范围,此示例请求了
openid
、email
、profile
以及获取用户信息所需的权限,多个权限之间用 URL 编码的空格(%20)分隔。 - context_ur: 可选参数,指定请求上下文地址(如有需要)。
用户授权:
用户在 Google 授权页面上同意授权后,Google 会将用户重定向回预先配置的redirect_uri
,并在 URL 中附带授权码(code)等参数。例如:https://collegebot.ai/google/login?state=AMbdmDkEVT5Bj0C_nXTrrL6_zmOE51bvsT_qPzj8WqXDx-XrP5yOsL4gWlO8VgWXeNws6jAhzoFjR0-eQgv4BaWcfN6HSTZoIlT_EZMvuUr-V4I_suq4_jV0RkmNg_eyYR28iaOXsJGrIoFcsN8c6DNiod8sKYVGigkU4tKaom08G1SeGJsTTHTWq0baflzsv0fxTskx8C3c8oxGEegdR_PSeOoRctXHZnf1wf6w3snuTv4lv0hyhqAR4QG1X9qa_BYdsDzvRkNqd5_9hwgEClewJN_m6Y0jEu5mjjcgedCfJNKhgfztV75V2zMamkynguqL3_U&code=4%2F0AQSTgQF2VH6M7n9X3wCwLN59tQquq1Ml_tDkHCJYCx6YJIvtXV0CcEvaxgTZx6vLkHgH2g&scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none
URL 参数说明:
- state: 与请求时传入的随机字符串保持一致,用于校验安全性。
- code: 授权后返回的授权码。该授权码有效期短且只能使用一次,后续用于交换访问令牌。
- scope: 表示实际授权的权限范围。
- authuser: 指定用户所选用的 Google 账户(如用户同时登录多个账号)。
- prompt: 控制授权页面的显示方式,值为
none
表示如果用户已登录且之前已授权,则直接返回授权码而不再弹出授权同意页面。
授权码交换:
前端获取授权码后,将其(通常经过 URL 解码)传递给后端。后端使用GOOGLE_CLIENT_ID
和GOOGLE_CLIENT_SECRET
向 Google 的 token 接口发起请求,交换出访问令牌(access token)和包含用户信息的id_token
。
注意: 在 token 请求中需要传递与初始请求时相同的redirect_uri
参数,用于验证请求的一致性,保证安全性。例如,通过
curl
发起如下请求:curl --location --request POST 'https://oauth2.googleapis.com/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'code=4/0AQSTgQF2VH6M7n9X3wCwLN59tQquq1Ml_tDkHCJYCx6YJIvtXV0CcEvaxgTZx6vLkHgH2g' \ --data-urlencode 'client_id=125062456843-b0o5sp6cm0fff067lanph5i9uf1lih3l.apps.googleusercontent.com' \ --data-urlencode 'client_secret=xxxx' \ --data-urlencode 'redirect_uri=https://collegebot.ai/google/login' \ --data-urlencode 'grant_type=authorization_code'
返回的 JSON 示例为:
{ "access_token": "ya29.a0AeXRPp7JJ5cTzccgFXXUZOSgTablJIcDyrmoKWdT3psZRZmStWM7PkTjzJgNqbocWft7E3DaTM4HiI4Cig0QJ5a4Ig7eoZt2kk2SqhqOt5rsVpQnsMrD9IWaN8-CYMV1kWDI-pRkFT5T3ndNuFbTSWgbEJb3JzOE_aR6OLNuvDwaCgYKAW8SARESFQHGX2MilqVn0sLMciCE5k7F2fLT5w0178", "expires_in": 3114, "scope": "https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email", "token_type": "Bearer", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjI1ZjgyMTE3MTM3ODhiNjE0NTQ3NGI1MDI5YjAxNDFiZDViM2RlOWMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMjUwNjI0NTY4NDMtYjBvNXNwNmNtMGZmZjA2N2xhbnBoNWk5dWYxbGloM2wuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIxMjUwNjI0NTY4NDMtYjBvNXNwNmNtMGZmZjA2N2xhbnBoNWk5dWYxbGloM2wuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDYyNjkzNzIzNTc2MTIxODY0OTkiLCJlbWFpbCI6ImxpdG9uZ2phdmEwMDFAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJUTG1XR0R3eWJpR2NEYmx3Ri1rUDRBIiwibmFtZSI6IlRvbmcgTGkiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jS3k1cXRYQ3JmdkdyTm95aEdZX0p2OGIzYUY5R2NCWHhvQU84NVJWTkxOZzg4ZHBDdz1zOTYtYyIsImdpdmVuX25hbWUiOiJUb25nIiwiZmFtaWx5X25hbWUiOiJMaSIsImlhdCI6MTc0MTUwODcyNSwiZXhwIjoxNzQxNTEyMzI1fQ.amUYFLpnE9ptysOtsD5rymStk_qvnACwrLeCq-an-2XXxSCR9n26exYtxSDava3if3YnNa-0z05XJA-cw5hsMEI4GNg47ffK_AuyyCPe_NxpEJ4190S9SgMm15EIvZfDIeTWWTh8TGF2bNR6JqD2nlLpL24q2JJoIjjo6GFUJGI6ZlQB5lHy71gO9rrhS3DTdKGMeDb0IP6fDNLdAkaFC2bCpN9XqO4ogYAaHshizm3cqgbPYBfKsqoRoDoLqraybzT78yoJ0-OFLcZlJ4yKpQvgZJS2AoGpK3BxXctsrs_3ed8TwNjD_giN_ogab3N4zj2OX-jdovjWfXPX0fu3-w" }
后端处理用户信息:
后端根据 Google 返回的用户信息(例如 email、用户 ID 等),判断该用户是否已存在于系统中:- 如果用户已存在,则直接生成并返回认证 token。
- 如果用户不存在,则创建新用户记录,再生成 token 返回给前端。
返回认证结果:
后端将生成的内部 token、refresh token(如有)以及用户标识返回给前端。后续前端访问接口时需要携带该 token 进行身份验证。
请求接口示例
后端最终返回给前端的 JSON 数据示例如下:
GET https://java-rumi-test.fly.dev/api/v1/google/login?code=4/0AQSTgQFQSBJG1CnKSzpOEX4CS5LvEDZ99Xt-oIHXRamxTI3vWGOr2U7g9tPQyHyw418awA&redirect_url=https://collegebot.ai/google/login
{
"data": {
"user_id": "488388937360424960",
"display_name": "Tong Li",
"email": "litongjava001@gmail.com",
"phone": null,
"photo_url": "https://lh3.googleusercontent.com/a/ACg8ocKy5qtXCrfvGrNoyhGY_Jv8b3aF9GcBXxoAO85RVNLNg88dpCw=s96-c",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi0xLCJ1c2VySWQiOiI0ODgzODg5MzczNjA0MjQ5NjAifQ==.VaziGyn3ellkiFChfWuU0m70mybJKN-yRtPKC0Twp78",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDI0OTY2NTAsInVzZXJJZCI6IjQ4ODM4ODkzNzM2MDQyNDk2MCJ9.bKHkuneji6zlS10bWLcZT-71Hhm7aWlv11FRSahftv8",
"expires_in": 1742496650
},
"ok": true,
"code": 1,
"msg": null,
"error": null
}
配置文件
在配置文件中,需提供 Google 的 Client ID 与 Client Secret,示例如下:
GOOGLE_CLIENT_ID=xx
GOOGLE_CLIENT_SECRET=xxx
注意: 请在生产环境中妥善保管这些敏感信息,避免泄露。
代码实现
下面给出一个基于 Java 的示例代码,实现 Google 登录的后端支持。示例代码包含以下主要部分:
- 实体类定义(用于解析 Google 返回的 token 和用户信息)
- 路由配置
- Google 登录 Handler(处理登录回调)
- Google 登录 Service(处理授权码交换及用户信息处理)
5.1 实体类
GoogleToken.java
用于解析从 Google 返回的 token 信息,其中包含 access_token、expires_in、scope、token_type 以及 id_token。
package com.litongjava.tio.boot.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain=true)
public class GoogleToken {
private String access_token;
private Integer expires_in;
private String scope;
private String token_type;
private String id_token;
}
GoogleJwtPayload.java
用于解析 JWT 中的负载部分,包含用户基本信息及 token 的元数据。
package com.litongjava.tio.boot.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class GoogleJwtPayload {
// 签发者
private String iss;
// 授权方
private String azp;
// 接收者
private String aud;
// 用户唯一标识
private String sub;
// 用户邮箱
private String email;
// 邮箱是否验证
private boolean emailVerified;
// 访问令牌哈希
private String atHash;
// 用户全名
private String name;
// 用户头像地址
private String picture;
// 名字
private String givenName;
// 姓氏
private String familyName;
// 签发时间(Unix 时间戳)
private long iat;
// 过期时间(Unix 时间戳)
private long exp;
}
5.2 路由配置
将 Google 登录的回调地址映射到相应的 Handler 方法,例如:
r.add("/api/v1/google/login", appUserGoogleHandler::login);
5.3 处理逻辑:Google 登录 Handler
Google 登录 Handler 用于接收前端传递的授权码,并调用 Service 层进行登录处理,示例代码如下:
package com.litongjava.tio.boot.admin.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.tio.boot.admin.services.AppUserGoogleService;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
public class AppUserGoogleHandler {
/**
* 处理 Google 登录回调
* 前端将获取的授权码通过 query 参数传递过来
*/
public HttpResponse login(HttpRequest request) {
// 获取授权码参数
String code = request.getParameter("code");
String redirect_url = request.getParam("redirect_url");
HttpResponse response = TioRequestContext.getResponse();
// 获取 Google 登录服务实例
AppUserGoogleService googleService = Aop.get(AppUserGoogleService.class);
// 使用授权码处理登录流程
RespBodyVo vo = googleService.login(code, redirect_url);
return response.setJson(vo);
}
}
5.4 服务层实现:Google 登录 Service
Service 层负责与 Google 进行交互,完成授权码交换、用户信息处理、用户记录检查或创建以及内部 token 签发,示例代码如下:
package com.litongjava.tio.boot.admin.services;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.model.http.response.ResponseVo;
import com.litongjava.tio.boot.admin.costants.TioBootAdminTableNames;
import com.litongjava.tio.boot.admin.vo.AppUserLoginVo;
import com.litongjava.tio.boot.admin.vo.GoogleJwtPayload;
import com.litongjava.tio.boot.admin.vo.GoogleToken;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.http.HttpUtils;
import com.litongjava.tio.utils.json.FastJson2Utils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import okhttp3.FormBody;
import okhttp3.Request;
public class AppUserGoogleService {
/**
* 使用 Google 授权码进行登录
*/
public RespBodyVo login(String code, String redirectUri) {
// 从配置中读取 Google 的 Client ID 和 Client Secret
String clientId = EnvUtils.get("GOOGLE_CLIENT_ID");
String clientSecret = EnvUtils.get("GOOGLE_CLIENT_SECRET");
// 构建请求 URL(Google 的 token 交换接口)
String tokenUrl = "https://oauth2.googleapis.com/token";
// 构建请求参数
FormBody.Builder builder = new FormBody.Builder();
builder.add("code", code).add("redirect_uri", redirectUri).add("grant_type", "authorization_code")
//
.add("client_id", clientId).add("client_secret", clientSecret);
FormBody formBody = builder.build();
Request request = new Request.Builder().url(tokenUrl).post(formBody).build();
ResponseVo responseVo = HttpUtils.call(request);
if (responseVo.isOk()) {
// 根据 tokenResponse 解析出 access token 与用户信息(需要根据实际返回格式解析)
String tokenResponse = responseVo.getBodyString();
// 解析 tokenResponse,获得 googleId 并同步 google_info 信息
GoogleJwtPayload googlePayload = parseGoogleId(tokenResponse);
String googleId = googlePayload.getSub();
// 检查数据库中是否存在该用户(这里建议根据 google_id 字段查找)
Row row = Db.findFirst("SELECT id FROM app_users WHERE google_id = ?", googleId);
String userId = null;
String name = googlePayload.getName();
String photo_url = googlePayload.getPicture();
String email = googlePayload.getEmail();
if (row == null) {
Row emailRow = Db.findFirst("SELECT id,email FROM app_users WHERE email = ?", email);
if (emailRow == null) {
long longId = SnowflakeIdUtils.id();
userId = String.valueOf(longId);
Row user = Row.by("id", userId).set("email", email).set("display_name", name).set("photo_url", photo_url);
Db.save(TioBootAdminTableNames.app_users, user);
} else {
userId = emailRow.getString("id");
}
} else {
userId = row.getString("id");
}
// 生成系统内部 token,有效期 7 天(604800秒)
Long timeout = EnvUtils.getLong("app.token.timeout", 604800L);
Long tokenTimeout = System.currentTimeMillis() / 1000 + timeout;
AppUserService appUserService = Aop.get(AppUserService.class);
String token = appUserService.createToken(userId, tokenTimeout);
String refreshToken = appUserService.createRefreshToken(userId);
AppUserLoginVo appUserLoginVo = new AppUserLoginVo(userId, name, email, photo_url, refreshToken, token, tokenTimeout.intValue());
return RespBodyVo.ok(appUserLoginVo);
} else {
return RespBodyVo.fail(responseVo.getBodyString());
}
}
/**
* 根据 tokenResponse 解析出 Google 用户唯一标识
*/
public GoogleJwtPayload parseGoogleId(String tokenResponse) {
GoogleToken googleToken = FastJson2Utils.parse(tokenResponse, GoogleToken.class);
String idToken = googleToken.getId_token();
// 解码 JWT 的 payload 部分
String[] tokenParts = idToken.split("\\.");
if (tokenParts.length < 2) {
throw new IllegalArgumentException("无效的 id_token");
}
// 注意:JWT 分为 header.payload.signature,此处只需要 payload
byte[] decodedBytes = Base64.getUrlDecoder().decode(tokenParts[1]);
String payloadJsonStr = new String(decodedBytes, StandardCharsets.UTF_8);
return JsonUtils.parse(payloadJsonStr, GoogleJwtPayload.class);
}
}
说明:
配置读取:
通过EnvUtils.get()
方法获取配置文件中的GOOGLE_CLIENT_ID
与GOOGLE_CLIENT_SECRET
,确保敏感信息不被硬编码。授权码交换:
使用 HTTP 客户端工具向 Google 的 token 接口发起请求,请求参数包括授权码、Client ID、Client Secret、redirect_uri 及 grant_type。具体实现请参考 Google 的 OAuth 2.0 文档。用户信息处理:
解析 Google 返回的 tokenResponse,利用parseGoogleId
方法获取用户的唯一标识(googleId),并通过数据库查询判断用户是否存在。如果不存在,则创建新用户记录;若已存在,则继续使用已有的用户信息。内部 Token 签发:
生成系统内部的 token 和 refresh token,并设置有效期(示例中为 7 天)。
总结
本文档详细描述了 Google 登录的整体流程和后端支持逻辑。通过以下步骤实现 Google 登录:
- 配置 Google Cloud Console,获取并设置正确的 Client ID 与 Client Secret。
- 前端通过点击 Google 登录按钮发起 OAuth 授权请求,并在用户授权后获得临时授权码(code)。
- 后端接收授权码,并通过与 Google OAuth 2.0 服务交互,完成授权码到访问令牌的交换。
- 解析返回的 id_token 获取用户信息,根据数据库检查该用户是否存在,并生成内部认证 token。
- 将认证 token、refresh token 以及用户标识返回给前端,实现安全便捷的登录功能。
本示例代码基于 Java 实现,适用于构建后端支持逻辑,可根据实际业务需求进行扩展和调整。希望本文档能为您提供全面且详细的参考,助力项目快速实现 Google 登录功能。