本文介绍如何将 Google OAuth 2.0 登录集成到你的网站,涵盖 Google Cloud 控制台配置、前端接入、后端验证及安全最佳实践。
Google 登录采用 OAuth 2.0 + OpenID Connect 协议。整体流程如下:
code 或 id_token 的回调请求发送回你的网站| 字段 | 说明 |
|---|---|
| 应用名称 | 显示在授权弹窗中的名称 |
| 用户支持邮箱 | 用户授权问题的联系邮箱 |
| 开发者联系邮箱 | Google 通知开发者的邮箱 |
| 授权域名 | 你的生产环境域名,如 example.com |
openidemailprofile已授权的 JavaScript 来源(用于前端 One Tap 或 OAuth):
http://localhost:3000 ← 本地开发
https://example.com ← 生产环境
已授权的重定向 URI(用于授权码模式回调):
http://localhost:3000/auth/google/callback
https://example.com/auth/google/callback
在项目根目录创建 .env 文件(切勿提交至 Git):
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_CALLBACK_URL=https://example.com/auth/google/callback
SESSION_SECRET=your-random-session-secret
Google One Tap 是最现代的接入方式,弹出式授权,无需页面跳转。
在 HTML <head> 中引入脚本:
<script src="https://accounts.google.com/gsi/client" async></script>
在页面中添加:
<!-- 初始化配置(隐藏元素) -->
<div id="g_id_onload"
data-client_id="YOUR_GOOGLE_CLIENT_ID"
data-callback="handleCredentialResponse"
data-auto_prompt="false">
</div>
<!-- 显示按钮 -->
<div class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left">
</div>
处理回调的 JavaScript:
function handleCredentialResponse(response) {
// response.credential 是一个 JWT id_token
fetch('/auth/google/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: response.credential }),
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.location.href = '/dashboard';
}
});
}
适合需要请求额外权限(如 Google Drive、Gmail)的场景。
前端只需一个按钮,点击跳转后端路由:
<a href="/auth/google" class="btn-google-login">
使用 Google 账号登录
</a>
npm install passport passport-google-oauth20 express-session
// config/passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
}, async (accessToken, refreshToken, profile, done) => {
try {
// 在数据库中查找或创建用户
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
});
}
return done(null, user);
} catch (err) {
return done(err);
}
}));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});
// routes/auth.js
const express = require('express');
const passport = require('passport');
const router = express.Router();
// 发起 Google 授权
router.get('/auth/google',
passport.authenticate('google', {
scope: ['openid', 'email', 'profile'],
})
);
// Google 回调处理
router.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login?error=1' }),
(req, res) => {
res.redirect('/dashboard');
}
);
// 登出
router.post('/auth/logout', (req, res) => {
req.logout(() => res.redirect('/'));
});
module.exports = router;
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
router.post('/auth/google/verify', async (req, res) => {
const { credential } = req.body;
try {
const ticket = await client.verifyIdToken({
idToken: credential,
audience: process.env.GOOGLE_CLIENT_ID,
});
const payload = ticket.getPayload();
const { sub: googleId, email, name, picture } = payload;
// 查找或创建用户
let user = await User.findOne({ googleId });
if (!user) {
user = await User.create({ googleId, email, name, avatar: picture });
}
// 建立会话
req.session.userId = user.id;
res.json({ success: true });
} catch (err) {
res.status(401).json({ success: false, error: '无效凭据' });
}
});
pip install social-auth-app-django
在 settings.py 中配置:
INSTALLED_APPS += ['social_django']
AUTHENTICATION_BACKENDS = [
'social_core.backends.google.GoogleOAuth2',
'django.contrib.auth.backends.ModelBackend',
]
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'YOUR_CLIENT_ID'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'YOUR_CLIENT_SECRET'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['openid', 'email', 'profile']
LOGIN_URL = '/auth/login/'
LOGIN_REDIRECT_URL = '/dashboard/'
在 urls.py 中:
urlpatterns = [
path('auth/', include('social_django.urls', namespace='social')),
# ...
]
前端登录链接:
<a href="{% url 'social:begin' 'google-oauth2' %}">使用 Google 登录</a>
| 验证项 | 说明 |
|---|---|
aud(受众) |
必须等于你的 Client ID,防止 Token 被其他应用伪造 |
iss(颁发者) |
必须是 accounts.google.com 或 https://accounts.google.com |
exp(过期时间) |
Token 不能已过期 |
| HTTPS | 所有通信必须走 HTTPS,绝不在 HTTP 下传输 Token |
在重定向流程中,使用 state 参数防止 CSRF 攻击:
// 生成随机 state 存入 session
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
// 发起授权时带上 state
passport.authenticate('google', { scope: [...], state })
回调时验证:
if (req.query.state !== req.session.oauthState) {
return res.status(403).send('CSRF 验证失败');
}
SESSION_SECRETaccess_token 的明文,如需长期访问应使用 refresh_token 并加密| 错误信息 | 原因 | 解决方法 |
|---|---|---|
redirect_uri_mismatch |
回调 URI 与控制台配置不符 | 检查控制台配置的重定向 URI 是否与代码完全一致(含协议、端口、路径) |
invalid_client |
Client ID 或 Secret 错误 | 重新复制凭据,确认环境变量正确加载 |
access_denied |
用户拒绝授权或应用未通过审查 | 外部应用需完成 Google 审查流程 |
| Token 验证失败 | aud 不匹配或 Token 过期 |
检查 Client ID 是否正确,系统时间是否同步 |
This app isn't verified |
应用未完成 Google 审查 | 开发阶段可将测试账号加入测试用户列表;生产环境需提交审查 |
用户点击登录
↓
浏览器跳转 Google 授权页(携带 client_id、scope、redirect_uri、state)
↓
用户同意授权
↓
Google 重定向回 callback URL(携带 code 或 id_token)
↓
后端用 code 换取 access_token + id_token(授权码模式)
或 直接验证 id_token(One Tap 模式)
↓
解析 id_token,获取用户信息
↓
数据库查找或创建用户 → 建立本地会话
↓
重定向至应用主页 ✓