OAuth返回的数据解析
id:用户的Google ID。可以唯一标识用户。
email:用户的邮箱地址。
name:用户的全名。
picture:用户的头像URL。
locale:用户的语言和地区信息。
客户端ID 和 客户端密钥
CLIENT_SECRETS_FILE 是什么,有什么用?
CLIENT_SECRETS_FILE 是一个 JSON 格式的文件,通常是你在 Google Developer Console 上创建 OAuth 2.0 客户端时下载的文件,里面包含了你应用的 客户端 ID 和 客户端密钥。
客户端ID(Client ID):这是唯一标识你应用的字符串,Google用它来识别是哪个应用在请求用户数据。
客户端密钥(Client Secret):是一个保密的密钥,用来验证你的应用身份。这个密钥非常重要,不能泄露。
html
auth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth?"
f"client_id={client_id}&"
f"redirect_uri={REDIRECT_URI}&"
f"response_type=code&"
f"scope=openid%20email%20profile&"
f"access_type=offline"
)
client_id:应用的唯一标识符。
redirect_uri:授权成功后,Google将用户重定向回的地址,附带授权码。
(为什么Google需要验证回调地址?:Google只会把 授权码 返回到你在 Google 开发者平台上注册的 回调地址。如果 回调地址不一致,Google就知道请求是假的,直接拒绝。这样,只有你注册的合法应用才能接收用户的授权数据。)
html
Google Developer Console 中的 redirect_uri:告诉 Google 这是你应用的有效回调地址,Google会在授权后只允许将用户重定向到这个地址。
OAuth 请求中的 redirect_uri:你发起授权请求时,告诉 Google 完成授权后,用户应该重定向到哪里。Google会验证这个地址是否和你在开发者平台中注册的地址一致,以确保请求是安全的。
response_type=code:表示请求的是授权码。
scope:定义请求的权限范围,例如获取用户的电子邮件和基本资料。
access_type=offline:表示请求长期访问权限,并获取刷新令牌。
jwt和session
session 不是一个"令牌",它是一个 会话标识符,用于识别和管理用户的会话信息。
JWT 是一个"令牌",它是 自包含的身份信息,用于在不同的服务之间传递认证信息。
代码
python
from flask import Flask, render_template, redirect, url_for, session, request
from google.oauth2 import id_token
from google.auth.transport.requests import Request
import os
import sqlite3
import json
from datetime import datetime
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # 改成随机字符串
# Google OAuth 配置
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
# 从 client_secret.json 加载配置
CLIENT_SECRETS_FILE = "client_secret.json"
# 数据库文件
DB_FILE = "users.db"
# Google 令牌交换 URL
TOKEN_URI = 'https://oauth2.googleapis.com/token'
REDIRECT_URI = 'http://127.0.0.1:5000/callback'
# ==================== 数据库相关 ====================
def init_db():
"""初始化数据库,创建用户表"""
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
google_id TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
name TEXT,
picture TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def get_db_connection():
"""获取数据库连接"""
conn = sqlite3.connect(DB_FILE)
conn.row_factory = sqlite3.Row
return conn
def find_or_create_user(google_id, email, name, picture):
"""
查找用户,如果不存在则创建,存在则更新
返回用户信息
"""
conn = get_db_connection()
cursor = conn.cursor()
# 查找用户
cursor.execute('SELECT * FROM users WHERE email = ? OR google_id = ?', (email, google_id))
user = cursor.fetchone()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if user:
# 用户存在,更新 name 和 picture
cursor.execute('''
UPDATE users
SET name = ?, picture = ?, updated_at = ?
WHERE id = ?
''', (name, picture, now, user['id']))
conn.commit()
# 返回更新后的用户
cursor.execute('SELECT * FROM users WHERE id = ?', (user['id'],))
user = cursor.fetchone()
else:
# 用户不存在,插入新用户
cursor.execute('''
INSERT INTO users (google_id, email, name, picture, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
''', (google_id, email, name, picture, now, now))
conn.commit()
# 返回新插入的用户
cursor.execute('SELECT * FROM users WHERE email = ?', (email,))
user = cursor.fetchone()
conn.close()
return dict(user)
# ==================== 路由 ====================
@app.route('/')
def index():
return render_template('login.html')
@app.route('/login')
def login():
# 读取 client_secret.json 获取 client_id
with open(CLIENT_SECRETS_FILE, 'r') as f:
client_config = json.load(f)
client_id = client_config['web']['client_id']
# 构建 Google 登录 URL
auth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth?"
f"client_id={client_id}&"
f"redirect_uri={REDIRECT_URI}&"
f"response_type=code&"
f"scope=openid%20email%20profile&"
f"access_type=offline"
)
return redirect(auth_url)
@app.route('/callback')
def callback():
code = request.args.get('code')
if not code:
return 'No code provided', 400
# 读取 client_secret.json 获取凭据
with open(CLIENT_SECRETS_FILE, 'r') as f:
client_config = json.load(f)
client_id = client_config['web']['client_id']
client_secret = client_config['web']['client_secret']
# 交换 code 获取 token
token_data = {
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI
}
response = requests.post(TOKEN_URI, data=token_data)
if response.status_code != 200:
return f'Error exchanging code: {response.text}', 400
tokens = response.json()
id_token_str = tokens['id_token']
# 验证 id_token 获取用户信息
id_info = id_token.verify_oauth2_token(
id_token_str,
Request(),
audience=client_id
)
# 从 id_token 中提取用户信息
google_id = id_info.get('sub') # Google 用户唯一ID
email = id_info.get('email')
name = id_info.get('name', '')
picture = id_info.get('picture', '')
# 保存或更新用户到数据库
user = find_or_create_user(google_id, email, name, picture)
# 保存 email 到 session
session['email'] = email
return redirect(url_for('profile'))
@app.route('/profile')
def profile():
if 'email' not in session:
return redirect(url_for('index'))
# 从数据库获取用户信息
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE email = ?', (session['email'],))
user = cursor.fetchone()
conn.close()
if not user:
session.clear()
return redirect(url_for('index'))
return f'''
<h1>登录成功!</h1>
<p>欢迎, {user['name']}</p>
<p>邮箱: {user['email']}</p>
<p>Google ID: {user['google_id']}</p>
<img src="{user['picture']}" alt="头像" width="100">
<br><br>
<a href="/logout">退出登录</a>
'''
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('index'))
# 导入 requests(之前漏了)
import requests
if __name__ == '__main__':
# 初始化数据库
init_db()
app.run(debug=True, port=5000)
login.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Google 登录</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
text-align: center;
}
h1 {
color: #333;
margin-bottom: 20px;
}
.google-btn {
display: inline-block;
background: #4285f4;
color: white;
padding: 12px 30px;
border-radius: 5px;
text-decoration: none;
font-size: 16px;
transition: background 0.3s;
}
.google-btn:hover {
background: #3367d6;
}
.google-icon {
width: 20px;
height: 20px;
vertical-align: middle;
margin-right: 10px;
}
</style>
</head>
<body>
<div class="login-container">
<h1>欢迎登录</h1>
<p>使用 Google 账号登录</p>
<a href="/login" class="google-btn">
<img src="https://www.google.com/favicon.ico" class="google-icon" alt="Google">
使用 Google 账号登录
</a>
</div>
</body>
</html>
总结
OAuth 2.0 流程总结:
1. 用户点击 Google 登录按钮
-
用户在你的应用中点击 Google 登录 按钮。
-
应用通过生成一个 URL,重定向用户到 Google 的 OAuth 授权页面,URL 中包含:
-
client_id(你在 Google 开发者控制台注册时获得的客户端ID) -
redirect_uri(用户授权后,Google会将用户重定向到这个地址) -
scope(请求的权限范围,例如openid,email,profile) -
response_type=code(请求授权码)
-
示例URL:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=YOUR_CLIENT_ID&
redirect_uri=http://127.0.0.1:5000/callback\&
response_type=code&
scope=openid%20email%20profile&
access_type=offline
2. 用户登录并授权
- 用户在Google页面登录,并授权你的应用访问他们的Google账户数据(如电子邮件、姓名等)。
3. Google 重定向回你的应用并附带授权码
- 授权成功后,Google会将用户重定向到你的
redirect_uri(如:http://127.0.0.1:5000/callback),并附带一个 授权码(code),类似于:
http://127.0.0.1:5000/callback?code=AUTHORIZATION_CODE
4. 交换授权码为访问令牌
-
你的应用接收到授权码后,会使用它向 Google的Token API 请求 访问令牌(access token)。
-
请求参数包括:
-
client_id和client_secret(这两个参数用来验证你的应用) -
code(刚才从Google回调中获取的授权码) -
grant_type=authorization_code(表示你使用的是授权码流程) -
redirect_uri(这个URI必须和请求时使用的重定向URI一致)
-
请求示例:
token_data = {
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI
}
- Google会返回一个 access_token (访问令牌),和一个 id_token(用户身份令牌)。
5. 使用访问令牌获取用户数据
-
你可以使用
access_token向 Google API 请求用户的基本信息,如:姓名、电子邮件等。 -
例如,向 Google用户信息API 发起请求:
headers = {
'Authorization': f'Bearer {access_token}'
}
user_info_response = requests.get('https://www.googleapis.com/oauth2/v1/userinfo', headers=headers)
user_info = user_info_response.json()
6. 保存用户数据到 SQLite 数据库
-
将获取到的 用户信息(如:姓名、邮箱、头像)保存到 SQLite 数据库中。你可以检查用户是否已经存在,如果存在则更新,如果不存在则插入新记录。
-
使用
sqlite3插入或更新用户数据:
def find_or_create_user(google_id, email, name, picture):
在数据库中查找用户,若无则插入新用户
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE email = ? OR google_id = ?', (email, google_id))
user = cursor.fetchone()
if user:
更新用户信息
cursor.execute('UPDATE users SET name = ?, picture = ? WHERE id = ?', (name, picture, user['id']))
else:
插入新用户
cursor.execute('INSERT INTO users (google_id, email, name, picture) VALUES (?, ?, ?, ?)', (google_id, email, name, picture))
conn.commit()
conn.close()
7. 使用 Session 管理用户状态
-
你可以将用户的 邮箱 或 用户ID 保存在 Flask的
session中,用来管理用户的登录状态。 -
用户在接下来的请求中,Flask会通过
session自动识别用户的身份。
session['email'] = email # 保存用户邮箱到 session
8. 跳转到用户的个人资料页面
- 登录成功后,重定向到一个
/profile路由,展示用户的信息。
return redirect(url_for('profile'))
9. 用户退出登录
- 用户点击 退出登录 时,清除
session中的用户信息,并重定向回主页。
@app.route('/logout')
def logout():
session.clear() # 清除 session
return redirect(url_for('index')) # 返回主页
123