跳至正文

🔐 什么是 OAuth 2.0?

📖 定义

OAuth 2.0 是一个开放标准协议,允许用户在不共享密码的情况下授权其他网站或应用程序访问其信息。"使用 Google 登录"、"使用 GitHub 登录"等社交登录是典型示例。它专注于授权(Authorization)而非认证(Authentication),提供安全便捷的用户体验。

🎯 通过比喻理解

酒店钥匙卡

传统方式 = 酒店万能钥匙
酒店老板: "这是万能钥匙"
你: "这能打开所有房间! 😱"
"这就像泄露我的密码一样"

OAuth 2.0 = 酒店钥匙卡
酒店老板: "这张钥匙卡..."
✅ 只能打开你的房间
✅ 退房时自动失效
✅ 丢失也不影响万能钥匙的安全
✅ 随时可以撤销

你: "完美!"

授权委托书

场景: 委托代理人办理银行业务

❌ 密码共享方式
你: "我的银行密码是 1234"
代理人: "我可以在银行做任何事!"
风险: 可以提款、删除账户等所有权限

✅ OAuth 方式
你: "去银行开具授权委托书"
银行: "要授予什么权限?"
你: "只允许查询余额"
银行: "期限呢?"
你: "只要一周"

代理人可以:
✅ 只能查询余额
✅ 一周后自动失效
✅ 不知道你的密码
✅ 随时可以撤销权限

⚙️ 工作原理

1. OAuth 2.0 关键概念

┌─────────────────────────────────────────┐
│ OAuth 2.0 四个角色 │
└─────────────────────────────────────────┘

1. Resource Owner (资源所有者)
└─ 用户 (你)

2. Client (客户端)
└─ 想要使用服务的应用程序
└─ 例如: 你创建的网站

3. Authorization Server (授权服务器)
└─ 授予权限的服务器
└─ 例如: Google, GitHub, Naver

4. Resource Server (资源服务器)
└─ 持有用户信息的服务器
└─ 例如: Google API, GitHub API

2. OAuth 2.0 认证流程 (Authorization Code)

┌──────────┐                                 ┌──────────┐
│ 用户 │ │ 客户端 │
│ (你) │ │ (Web应用)│
└─────┬────┘ └────┬─────┘
│ │
│ 1. 点击"使用 Google 登录" │
│──────────────────────────────────────────>│
│ │
│ 2. 重定向到 Google 登录页面 │
│<──────────────────────────────────────────│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ 3. 登录 │ (授权服务器) │ │
│────────>│ │ │
│ │ │ │
│ 4. 权限请求 │
│ "此应用想要查看你的邮箱 │
│ 和个人资料" │
│<────────│ │ │
│ │ │ │
│ 5. 批准 │ │ │
│────────>│ │ │
│ │ │ │
│ │ 6. 发放 Authorization Code │
│ │ (一次性代码) │
│<────────│ │ │
│ └──────────────────┘ │
│ │
│ 7. 传递 Authorization Code │
│──────────────────────────────────────────>│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ │ (授权服务器) │ 8. 用 Code │
│ │ │<─── 和 Client│
│ │ │ Secret │
│ │ │ 交换 │
│ │ │ │
│ │ 9. 发放 Access │ ────────────>│
│ │ Token │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Google API │ │
│ │ (资源服务器) │ 10. 用Token │
│ │ │<──── 请求 │
│ │ │ 用户信息 │
│ │ │ │
│ │ 11. 用户信息 │ ────────────>│
│ │ 响应 │ │
│ └──────────────────┘ │
│ │
│ 12. 登录完成! │
│<──────────────────────────────────────────│
│ │

关键点:
- 用户密码只在 Google 输入
- Web应用永远不知道密码
- 只能通过 Access Token 访问授权的信息

3. OAuth 2.0 授权类型

========== 1. Authorization Code (最安全) ==========
用途: 服务器端 Web 应用程序
优点: 最安全 (使用 Client Secret)
示例: "使用 Google 登录"

流程:
1. 用户 → Google 登录
2. Google → 发放 Authorization Code
3. 服务器 → 用 Code + Secret 换取 Access Token
4. 服务器 → 用 Token 调用 API

========== 2. Implicit (简单但不太安全) ==========
用途: 纯浏览器应用 (SPA)
优点: 简单 (不需要服务器)
缺点: 安全漏洞 (Token 暴露在 URL 中)
示例: 旧式 SPA
⚠️ 现在不推荐使用

========== 3. Resource Owner Password (遗留) ==========
用途: 仅限可信应用
优点: 简单
缺点: 应用知道用户密码
示例: 自家移动应用
⚠️ 违背 OAuth 理念,不推荐

========== 4. Client Credentials ==========
用途: 服务器间通信 (M2M)
优点: 不需要用户认证
示例: 后端服务间通信

流程:
1. 服务器 → 发送 Client ID + Secret
2. Authorization Server → 发放 Access Token
3. 服务器 → 用 Token 调用 API

========== 5. PKCE (Proof Key for Code Exchange) ==========
用途: 移动应用、SPA (目前推荐)
优点: Authorization Code + 额外安全性
示例: 现代移动/SPA 应用

流程:
1. 应用 → 生成 code_verifier (随机字符串)
2. 应用 → 生成 code_challenge (verifier 哈希)
3. 应用 → 带 challenge 请求 Authorization Code
4. Google → 发放 Code
5. 应用 → 用 Code + verifier 换取 Token
6. Google → 验证 verifier 后发放 Token

💡 实际示例

Google OAuth 2.0 登录 (Node.js)

// ========== 1. Google Cloud Console 设置 ==========
/*
1. 访问 https://console.cloud.google.com
2. 创建项目
3. "API 和服务" → "凭据"
4. 创建 "OAuth 2.0 客户端 ID"
5. 添加已授权的重定向 URI:
http://localhost:3000/auth/google/callback
6. 获取 Client ID 和 Client Secret
*/

// ========== 2. 服务器实现 ==========
const express = require('express');
const axios = require('axios');
const session = require('express-session');

const app = express();

// 会话设置
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true
}));

// Google OAuth 配置
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback';

// ========== 步骤 1: 开始登录 ==========
app.get('/auth/google', (req, res) => {
// 创建 Google 认证 URL
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';

const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code', // 请求 Authorization Code
scope: 'openid email profile', // 请求的权限
access_type: 'offline', // 获取 Refresh Token
prompt: 'consent' // 总是显示同意屏幕
});

// 重定向到 Google 登录页面
res.redirect(`${authUrl}?${params}`);
});

// ========== 步骤 2: 处理回调 ==========
app.get('/auth/google/callback', async (req, res) => {
const { code, error } = req.query;

// 用户拒绝
if (error) {
return res.status(400).send(`认证失败: ${error}`);
}

try {
// 用 Authorization Code 换取 Access Token
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
}
);

const {
access_token,
refresh_token,
expires_in,
token_type
} = tokenResponse.data;

console.log('Access Token:', access_token);
console.log('过期时间:', expires_in, '秒');

// 用 Access Token 获取用户信息
const userResponse = await axios.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${access_token}`
}
}
);

const user = userResponse.data;
/*
{
id: "123456789",
email: "user@gmail.com",
verified_email: true,
name: "张三",
picture: "https://lh3.googleusercontent.com/...",
locale: "zh-CN"
}
*/

// 将用户信息保存到会话
req.session.user = {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
accessToken: access_token,
refreshToken: refresh_token
};

// 保存用户到数据库
await saveOrUpdateUser(user);

// 重定向到主页
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth 错误:', error.response?.data || error.message);
res.status(500).send('认证处理过程中发生错误');
}
});

// ========== 查看用户信息 ==========
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}

res.send(`
<h1>你好,${req.session.user.name}!</h1>
<img src="${req.session.user.picture}" alt="头像" />
<p>邮箱: ${req.session.user.email}</p>
<a href="/logout">登出</a>
`);
});

// ========== 登出 ==========
app.get('/logout', async (req, res) => {
// 撤销 Google Access Token (可选)
const accessToken = req.session.user?.accessToken;
if (accessToken) {
try {
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
} catch (error) {
console.error('Token 撤销失败:', error.message);
}
}

// 销毁会话
req.session.destroy();
res.redirect('/');
});

// ========== 主页 ==========
app.get('/', (req, res) => {
res.send(`
<h1>OAuth 2.0 演示</h1>
<a href="/auth/google">
<button>使用 Google 登录</button>
</a>
`);
});

app.listen(3000, () => {
console.log('服务器运行中: http://localhost:3000');
});

使用 Passport.js 简化实现

// ========== 使用 Passport.js ==========
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Passport 配置
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/callback'
},
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,
picture: profile.photos[0].value
});
}

done(null, user);
} catch (error) {
done(error, null);
}
}
));

// 会话序列化
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});

// Express 配置
app.use(passport.initialize());
app.use(passport.session());

// ========== 路由 ==========

// 开始 Google 登录
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);

// Google 回调
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);

// 登出
app.get('/logout', (req, res) => {
req.logout((err) => {
if (err) console.error(err);
res.redirect('/');
});
});

// 认证中间件
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}

// 受保护的路由
app.get('/dashboard', isAuthenticated, (req, res) => {
res.send(`你好,${req.user.name}!`);
});

GitHub OAuth 2.0

// ========== GitHub OAuth 设置 ==========
/*
1. 访问 https://github.com/settings/developers
2. 点击 "New OAuth App"
3. 填写信息:
- Application name: My App
- Homepage URL: http://localhost:3000
- Authorization callback URL: http://localhost:3000/auth/github/callback
4. 获取 Client ID 和 Client Secret
*/

const express = require('express');
const axios = require('axios');
const app = express();

const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/github/callback';

// ========== GitHub 登录 ==========
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';

const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email', // 请求的权限
state: Math.random().toString(36) // CSRF 防护
});

res.redirect(`${authUrl}?${params}`);
});

// ========== 处理回调 ==========
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;

try {
// 1. 获取 Access Token
const tokenResponse = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI
},
{
headers: { Accept: 'application/json' }
}
);

const { access_token } = tokenResponse.data;

// 2. 获取用户信息
const userResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`
}
});

const user = userResponse.data;
/*
{
login: "username",
id: 12345,
avatar_url: "https://avatars.githubusercontent.com/...",
name: "张三",
email: "user@example.com",
bio: "开发者",
public_repos: 50
}
*/

// 3. 获取邮箱 (单独请求)
const emailResponse = await axios.get('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${access_token}`
}
});

const primaryEmail = emailResponse.data.find(email => email.primary);

res.json({
user,
email: primaryEmail.email
});
} catch (error) {
console.error('GitHub OAuth 错误:', error.message);
res.status(500).send('认证失败');
}
});

app.listen(3000);

在 React 中使用 OAuth

// ========== React 客户端 ==========

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function App() {
const [user, setUser] = useState(null);

// 页面加载时检查用户信息
useEffect(() => {
checkAuth();
}, []);

async function checkAuth() {
try {
const response = await axios.get('/api/me', {
withCredentials: true // 包含 cookies
});
setUser(response.data);
} catch (error) {
console.log('未登录');
}
}

function handleGoogleLogin() {
// 导航到服务器的 OAuth 端点
window.location.href = 'http://localhost:3000/auth/google';
}

function handleGitHubLogin() {
window.location.href = 'http://localhost:3000/auth/github';
}

function handleLogout() {
window.location.href = 'http://localhost:3000/logout';
}

if (user) {
return (
<div>
<h1>你好,{user.name}!</h1>
<img src={user.picture} alt="头像" />
<p>{user.email}</p>
<button onClick={handleLogout}>登出</button>
</div>
);
}

return (
<div>
<h1>登录</h1>
<button onClick={handleGoogleLogin}>
🔵 使用 Google 登录
</button>
<button onClick={handleGitHubLogin}>
⚫ 使用 GitHub 登录
</button>
</div>
);
}

export default App;

使用 PKCE 增强安全性 (SPA/移动)

// ========== PKCE (Proof Key for Code Exchange) ==========
// 用于移动应用或 SPA

const crypto = require('crypto');

// ========== 1. 生成 Code Verifier ==========
function generateCodeVerifier() {
return base64URLEncode(crypto.randomBytes(32));
}

// ========== 2. 生成 Code Challenge ==========
function generateCodeChallenge(verifier) {
const hash = crypto.createHash('sha256')
.update(verifier)
.digest();
return base64URLEncode(hash);
}

function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

// ========== 3. 开始登录 (客户端) ==========
async function startPKCELogin() {
// 生成并保存 verifier
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);

// 生成 challenge
const codeChallenge = generateCodeChallenge(codeVerifier);

// Google 认证 URL
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
code_challenge: codeChallenge, // PKCE
code_challenge_method: 'S256' // SHA-256
});

window.location.href = `${authUrl}?${params}`;
}

// ========== 4. 处理回调 ==========
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

// 获取保存的 verifier
const codeVerifier = sessionStorage.getItem('code_verifier');

// Token 交换
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // PKCE Verifier
})
});

const { access_token } = await response.json();

// 删除 verifier
sessionStorage.removeItem('code_verifier');

return access_token;
}

🤔 常见问题

Q1. OAuth vs JWT vs Session?

A:

// ========== 1. Session (会话) ==========
// 传统方法

// 登录
app.post('/login', (req, res) => {
const { username, password } = req.body;

// 验证认证
if (isValidUser(username, password)) {
// 保存到会话
req.session.userId = user.id;
res.json({ message: '登录成功' });
}
});

// 检查认证
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: '需要登录' });
}

const user = getUserById(req.session.userId);
res.json(user);
});

优点: 简单,服务器完全控制
缺点: 需要服务器内存/存储,可扩展性低

// ========== 2. JWT (JSON Web Token) ==========
// 无状态认证

// 登录
app.post('/login', (req, res) => {
const { username, password } = req.body;

if (isValidUser(username, password)) {
// 创建 JWT
const token = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '1h' }
);

res.json({ token });
}
});

// 检查认证
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, SECRET_KEY);
const user = getUserById(decoded.userId);
res.json(user);
} catch (error) {
res.status(401).json({ error: '无效的 token' });
}
});

优点: 无状态,可扩展性好
缺点: 难以撤销,体积大

// ========== 3. OAuth 2.0 ==========
// 第三方认证和授权

// 用户使用 Google 登录
// → Google 发放 Access Token
// → 我们的应用用 Token 调用 Google API

优点:
- 不共享密码
- 细粒度权限控制
- 标准协议
- 支持社交登录

缺点:
- 复杂
- 依赖外部服务

// ========== 对比 ==========
特性 | Session | JWT | OAuth
------------|---------|----------|-------
存储位置 | 服务器 | 客户端 | 服务器
可扩展性 |||
安全性 |||
复杂度 |||
第三方认证 |||

// ========== 组合使用 ==========
// OAuth 登录后发放 JWT

app.get('/auth/google/callback', async (req, res) => {
// 1. 使用 Google OAuth 认证用户
const googleUser = await getGoogleUser(req.query.code);

// 2. 保存用户到我们的数据库
const user = await saveOrUpdateUser(googleUser);

// 3. 创建并返回 JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '7d' }
);

res.json({ token });
});

// 后续请求使用 JWT 认证
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json(req.user);
});

Q2. Access Token 和 Refresh Token 的区别?

A:

// ========== Access Token ==========
// 短期有效 (15分钟~1小时)
// 用于 API 请求

// ========== Refresh Token ==========
// 长期有效 (7天~30天)
// 用于刷新 Access Token

// ========== 实现示例 ==========

// 登录时发放两者
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);

// Access Token (15分钟)
const accessToken = jwt.sign(
{ userId: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);

// Refresh Token (7天)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);

// 将 Refresh Token 保存到数据库 (用于撤销)
await saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken });
});

// ========== API 请求 (Access Token) ==========
app.get('/api/data', authenticateAccess, (req, res) => {
res.json({ data: '重要数据' });
});

function authenticateAccess(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, ACCESS_SECRET);

if (decoded.type !== 'access') {
throw new Error('无效的 token 类型');
}

req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'access_token_expired',
message: '请使用 Refresh Token 刷新'
});
}

res.status(401).json({ error: '无效的 token' });
}
}

// ========== 刷新 Access Token ==========
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;

if (!refreshToken) {
return res.status(401).json({ error: '需要 Refresh Token' });
}

try {
// 1. 验证 Refresh Token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);

if (decoded.type !== 'refresh') {
throw new Error('无效的 token 类型');
}

// 2. 在数据库中检查 (是否已撤销)
const isValid = await isRefreshTokenValid(decoded.userId, refreshToken);
if (!isValid) {
throw new Error('已撤销的 Refresh Token');
}

// 3. 发放新的 Access Token
const newAccessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);

res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: '无效的 Refresh Token' });
}
});

// ========== 前端自动刷新 ==========
import axios from 'axios';

let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');

// Axios 拦截器
axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
}
);

axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

// Access Token 过期
if (error.response?.data?.error === 'access_token_expired' &&
!originalRequest._retry) {
originalRequest._retry = true;

try {
// 使用 Refresh Token 刷新
const response = await axios.post('/auth/refresh', {
refreshToken
});

accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);

// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token 也过期 → 登出
localStorage.clear();
window.location.href = '/login';
}
}

return Promise.reject(error);
}
);

// ========== 安全提示 ==========

// ✅ 好的做法
1. Access Token: 短期有效 (15分钟)
2. Refresh Token: 保存在数据库中以便撤销
3. Refresh Token 存储在 HttpOnly cookies 中
4. 必须使用 HTTPS
5. Refresh Token 轮换 (刷新时发放新 token)

// ❌ 不好的做法
1. Access Token 有效期太长
2. Refresh Token 存储在 localStorage
3. Refresh Token 无法撤销
4. 使用 HTTP

Q3. 什么是 Scope (权限范围)?

A:

// ========== Scope 概念 ==========
// 应用程序请求的权限范围

// ========== Google OAuth Scopes ==========
const scopes = [
'openid', // 基本信息
'email', // 邮箱
'profile', // 个人资料 (姓名、照片)
'https://www.googleapis.com/auth/drive.readonly', // Drive 读取
'https://www.googleapis.com/auth/gmail.send' // Gmail 发送
];

// 登录时请求权限
app.get('/auth/google', (req, res) => {
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: scopes.join(' ') // 用空格分隔
});

res.redirect(`${authUrl}?${params}`);
});

// 显示给用户的屏幕:
/*
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
My App 请求:

✓ 查看你的邮箱地址
✓ 查看基本个人资料信息
✓ 查看你的 Google Drive 文件
✓ 通过 Gmail 发送邮件

[允许] [拒绝]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
*/

// ========== GitHub OAuth Scopes ==========
const githubScopes = [
'user', // 用户信息
'user:email', // 邮箱 (包括私密)
'repo', // 完整仓库访问
'public_repo', // 仅公开仓库
'read:org', // 读取组织信息
'write:org' // 写入组织信息
];

app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email public_repo' // 只要必要的权限
});

res.redirect(`${authUrl}?${params}`);
});

// ========== 最小权限原则 ==========

// ❌ 不好: 请求超出需要的权限
scope: 'user repo admin:org' // 权限太多!

// ✅ 好: 只要必要的权限
scope: 'user:email' // 只需要邮箱

// ========== 动态 Scope 请求 ==========

// 初始登录: 只要基本信息
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile' // 只要基本信息
});

res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// 后续需要额外权限时
app.get('/auth/google/drive', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'https://www.googleapis.com/auth/drive.readonly',
prompt: 'consent' // 请求重新同意
});

res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// ========== 渐进式授权 ==========
// 根据需要逐步添加权限

// 步骤1: 登录 (基本信息)
scope: 'email profile'

// 步骤2: Google Drive 集成 (用户需要时)
scope: 'https://www.googleapis.com/auth/drive'

// 步骤3: Gmail 集成 (用户需要时)
scope: 'https://www.googleapis.com/auth/gmail.send'

// ========== Scope 验证 ==========

// 检查 Access Token 中包含的 scope
async function checkScope(accessToken, requiredScope) {
const response = await axios.get(
'https://www.googleapis.com/oauth2/v1/tokeninfo',
{ params: { access_token: accessToken } }
);

const grantedScopes = response.data.scope.split(' ');

if (!grantedScopes.includes(requiredScope)) {
throw new Error('权限不足');
}
}

// 使用示例
app.get('/api/drive/files', async (req, res) => {
const accessToken = req.headers.authorization?.split(' ')[1];

try {
// 检查 Drive 权限
await checkScope(accessToken, 'https://www.googleapis.com/auth/drive.readonly');

// 调用 Drive API
const files = await listDriveFiles(accessToken);
res.json(files);
} catch (error) {
res.status(403).json({
error: '权限不足',
message: '需要 Google Drive 访问权限'
});
}
});

Q4. OAuth 安全最佳实践?

A:

// ========== 1. 必须使用 HTTPS ==========
// ✅ 始终使用 HTTPS
const REDIRECT_URI = 'https://myapp.com/callback';

// ❌ 绝对禁止 HTTP
const REDIRECT_URI = 'http://myapp.com/callback'; // Token 被盗风险!

// ========== 2. State 参数 (CSRF 防护) ==========

// 开始登录
app.get('/auth/google', (req, res) => {
// 生成并保存随机 state
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;

const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
state // CSRF 防护
});

res.redirect(`${authUrl}?${params}`);
});

// 在回调中验证
app.get('/auth/google/callback', (req, res) => {
const { state, code } = req.query;

// 检查 state
if (state !== req.session.oauthState) {
return res.status(403).send('检测到 CSRF 攻击');
}

// 删除 state
delete req.session.oauthState;

// ... 继续处理
});

// ========== 3. 保护 Client Secret ==========

// ✅ 使用环境变量
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;

// ✅ 只在服务器使用
// 绝不暴露给前端!

// ❌ 不要硬编码
const CLIENT_SECRET = 'abc123xyz'; // 绝对禁止!

// ========== 4. Redirect URI 验证 ==========

// 只允许在 Google Cloud Console 注册的 URI
// ✅ 精确匹配的 URI
https://myapp.com/auth/callback

// ❌ 不使用通配符
https://myapp.com/*

// ❌ 开放重定向漏洞
https://myapp.com/callback?redirect=evil.com

// ========== 5. Token 存储 ==========

// ✅ 服务器: 环境变量或加密的数据库
await saveToken(userId, encryptToken(accessToken));

// ✅ 客户端: HttpOnly cookies (防止 XSS)
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true, // 仅 HTTPS
sameSite: 'strict',
maxAge: 3600000 // 1小时
});

// ❌ localStorage (XSS 漏洞)
localStorage.setItem('token', accessToken); // 危险!

// ========== 6. Token 过期时间 ==========

// ✅ 短期有效
const accessToken = jwt.sign(payload, secret, {
expiresIn: '15m' // 15分钟
});

// ✅ 使用 Refresh Token
const refreshToken = jwt.sign(payload, secret, {
expiresIn: '7d' // 7天
});

// ❌ 无过期时间
const token = jwt.sign(payload, secret); // 危险!

// ========== 7. 最小化 Scope ==========

// ✅ 只要必要的权限
scope: 'email profile'

// ❌ 请求所有权限
scope: 'https://www.googleapis.com/auth/drive' // 不必要

// ========== 8. 实现 Token 撤销 ==========

// 登出时撤销 token
app.get('/logout', async (req, res) => {
const accessToken = req.cookies.access_token;

// 撤销 Google token
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);

// 从数据库删除 Refresh Token
await deleteRefreshToken(req.user.id);

// 清除 cookie
res.clearCookie('access_token');

res.redirect('/');
});

// ========== 9. 使用 PKCE (移动/SPA) ==========

// 无需 Client Secret 增强安全性
const codeVerifier = generateRandomString(128);
const codeChallenge = base64URLEncode(sha256(codeVerifier));

// 认证请求时发送 challenge
const params = {
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};

// Token 交换时发送 verifier
const tokenParams = {
code,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // 代替 Secret
};

// ========== 10. 定期 Token 刷新 ==========

// 后台自动刷新
setInterval(async () => {
try {
const newAccessToken = await refreshAccessToken(refreshToken);
updateToken(newAccessToken);
} catch (error) {
console.error('Token 刷新失败:', error);
redirectToLogin();
}
}, 14 * 60 * 1000); // 每14分钟 (15分钟过期前)

Q5. 认证(Authentication) vs 授权(Authorization)?

A:

// ========== 认证 (Authentication) ==========
// "你是谁?" - 身份验证

// 示例: 登录
const user = await authenticateUser(username, password);
if (user) {
console.log('你是张三 ✓');
}

// ========== 授权 (Authorization) ==========
// "你能做什么?" - 权限验证

// 示例: 仅管理员访问
if (user.role === 'admin') {
console.log('可以使用管理员功能 ✓');
} else {
console.log('无权限 ✗');
}

// ========== OAuth 2.0 的角色 ==========

// OAuth 主要是授权 (Authorization)
// - 用户授予应用权限
// - "允许此应用查看我的 Google Drive"

// 但也用于认证 (Authentication)
// - OpenID Connect (OAuth 2.0 + 认证)
// - "使用 Google 登录"

// ========== 实际示例 ==========

// 步骤1: 认证 (Authentication)
app.post('/login', async (req, res) => {
const { username, password } = req.body;

// 验证用户身份
const user = await User.findOne({ username });

if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: '认证失败' });
}

// 认证成功 → 发放 token
const token = jwt.sign(
{
userId: user.id,
role: user.role // 包含权限信息
},
SECRET_KEY
);

res.json({ token });
});

// 步骤2: 授权 (Authorization)
app.get('/admin/users', authenticateToken, authorizeAdmin, (req, res) => {
// 仅管理员可访问
const users = await User.findAll();
res.json(users);
});

function authenticateToken(req, res, next) {
// 认证: 验证 token
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: '需要认证' });
}
}

function authorizeAdmin(req, res, next) {
// 授权: 检查管理员
if (req.user.role !== 'admin') {
return res.status(403).json({ error: '无权限' });
}
next();
}

// ========== 使用 OAuth 进行认证 + 授权 ==========

// 1. 认证 (OpenID Connect)
app.get('/auth/google/callback', async (req, res) => {
// 使用 Google OAuth 验证用户
const googleUser = await getGoogleUser(req.query.code);

// 认证完成 → 用户身份已验证
console.log('用户:', googleUser.email);

// 登录到我们的系统
const user = await findOrCreateUser(googleUser);
req.session.userId = user.id;
});

// 2. 授权 (Scope)
app.get('/api/drive/files', async (req, res) => {
// 检查 Google Drive 权限
const hasPermission = await checkGoogleScope(
req.user.accessToken,
'https://www.googleapis.com/auth/drive.readonly'
);

if (!hasPermission) {
return res.status(403).json({
error: '需要权限',
message: '请授予 Google Drive 访问权限'
});
}

// 有权限 → 调用 API
const files = await listDriveFiles(req.user.accessToken);
res.json(files);
});

// ========== 对比总结 ==========

认证 (Authentication)
- 问题: "你是谁?"
- 目的: 身份验证
- 示例:
* 输入密码
* 指纹识别
* 使用 Google 登录
- HTTP 状态码: 401 Unauthorized

授权 (Authorization)
- 问题: "你能做什么?"
- 目的: 访问权限验证
- 示例:
* 管理员权限
* Google Drive 读取权限
* OAuth Scope
- HTTP 状态码: 403 Forbidden

// 两者都需要!
// 1. 首先认证 (登录)
// 2. 然后授权检查 (允许访问)

🎓 下一步

理解 OAuth 2.0 后,可以学习:

  1. JWT 令牌 (文档待编写) - 基于令牌的认证
  2. 什么是 HTTPS? (文档待编写) - 安全通信
  3. 什么是 CORS? (文档待编写) - API 安全

实践练习

# ========== 1. Google OAuth 实践 ==========

# 创建项目
mkdir oauth-demo
cd oauth-demo
npm init -y

# 安装包
npm install express axios express-session dotenv

# 创建 .env 文件
cat > .env << EOF
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
SESSION_SECRET=your_session_secret
EOF

# 编写 server.js 后运行
node server.js

# 访问 http://localhost:3000

# ========== 2. Passport.js 实践 ==========

npm install passport passport-google-oauth20

# ========== 3. 实现多个社交登录 ==========

npm install passport-github2 passport-facebook

# 同时支持 Google、GitHub、Facebook

🎬 总结

OAuth 2.0 是现代 Web 的标准认证协议:

  • 安全性: 无需共享密码的安全认证
  • 便利性: 社交登录实现快速注册
  • 权限控制: 只授予必要的权限
  • 标准: 所有主要服务都支持

构建安全便捷的认证系统! 🔐