🔐 什么是 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);