Appearance
第三方登录集成 - 社交登录完整实现
1. 第三方登录概述
1.1 什么是第三方登录
第三方登录允许用户使用已有的社交账号(如Google、GitHub、Facebook)登录你的应用,无需创建新账户。它基于OAuth 2.0和OpenID Connect协议实现。
核心优势:
- 降低注册门槛: 用户无需记忆新密码
- 提高转化率: 简化注册流程
- 获取用户信息: 可获取用户头像、邮箱等信息
- 增强信任: 利用知名平台的信任度
1.2 常见第三方登录平台
typescript
// 主流平台
- Google
- GitHub
- Facebook
- Twitter/X
- Microsoft
- Apple
- LinkedIn
// 国内平台
- 微信
- QQ
- 微博
- 支付宝1.3 技术选型
bash
# Passport.js - Node.js认证中间件
npm install passport passport-google-oauth20 passport-github2 passport-facebook
# NextAuth.js - Next.js认证解决方案
npm install next-auth
# Auth0 - 认证即服务
npm install @auth0/nextjs-auth02. Google 登录集成
2.1 创建 Google OAuth 应用
bash
# 1. 访问 Google Cloud Console
https://console.cloud.google.com/
# 2. 创建项目
# 3. 启用API
- Google+ API
# 4. 创建OAuth 2.0凭据
- 应用类型: Web应用
- 授权重定向URI: http://localhost:3000/auth/google/callback
# 5. 获取
- Client ID
- Client Secret2.2 后端实现(Express + Passport)
bash
npm install passport passport-google-oauth20 express-sessiontypescript
// auth/google.strategy.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/auth/google/callback',
scope: ['profile', 'email']
},
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,
provider: 'google'
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);
// 序列化用户
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id: string, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error, null);
}
});typescript
// routes/auth.ts
import express from 'express';
import passport from 'passport';
const router = express.Router();
// Google登录
router.get('/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
// Google回调
router.get('/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
failureMessage: true
}),
(req, res) => {
// 登录成功
res.redirect('/dashboard');
}
);
// 登出
router.post('/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.redirect('/');
});
});
export default router;typescript
// server.ts
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import authRoutes from './routes/auth';
import './auth/google.strategy';
const app = express();
// Session配置
app.use(session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24小时
}
}));
// 初始化Passport
app.use(passport.initialize());
app.use(passport.session());
// 路由
app.use('/auth', authRoutes);
app.listen(3000);2.3 前端实现(React)
tsx
// GoogleLoginButton.tsx
export function GoogleLoginButton() {
const handleLogin = () => {
window.location.href = '/auth/google';
};
return (
<button
onClick={handleLogin}
className="flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-gray-50"
>
<img src="/google-icon.svg" alt="Google" className="w-5 h-5" />
<span>使用 Google 登录</span>
</button>
);
}2.4 Next.js 实现(NextAuth.js)
bash
npm install next-authtypescript
// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
})
],
callbacks: {
async signIn({ user, account, profile }) {
// 自定义登录逻辑
return true;
},
async jwt({ token, account, profile }) {
if (account) {
token.accessToken = account.access_token;
token.id = profile?.sub;
}
return token;
},
async session({ session, token }) {
session.user.id = token.id as string;
session.accessToken = token.accessToken as string;
return session;
}
},
pages: {
signIn: '/login'
}
});tsx
// components/GoogleLogin.tsx
import { signIn } from 'next-auth/react';
export function GoogleLogin() {
return (
<button onClick={() => signIn('google', { callbackUrl: '/dashboard' })}>
使用 Google 登录
</button>
);
}tsx
// pages/dashboard.tsx
import { useSession, signOut } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function Dashboard() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login');
}
}, [status]);
if (status === 'loading') {
return <div>Loading...</div>;
}
return (
<div>
<h1>Welcome {session?.user?.name}</h1>
<img src={session?.user?.image || ''} alt="Avatar" />
<button onClick={() => signOut()}>登出</button>
</div>
);
}3. GitHub 登录集成
3.1 创建 GitHub OAuth 应用
bash
# 1. 访问GitHub Settings
https://github.com/settings/developers
# 2. New OAuth App
- Application name: Your App Name
- Homepage URL: http://localhost:3000
- Authorization callback URL: http://localhost:3000/auth/github/callback
# 3. 获取
- Client ID
- Client Secret3.2 实现 GitHub 登录
typescript
// auth/github.strategy.ts
import passport from 'passport';
import { Strategy as GitHubStrategy } from 'passport-github2';
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: '/auth/github/callback',
scope: ['user:email']
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ githubId: profile.id });
if (!user) {
user = await User.create({
githubId: profile.id,
username: profile.username,
email: profile.emails?.[0]?.value,
avatar: profile.photos?.[0]?.value,
provider: 'github'
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);typescript
// NextAuth.js配置
import GitHubProvider from 'next-auth/providers/github';
export default NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!
})
]
});4. Facebook 登录集成
4.1 创建 Facebook 应用
bash
# 1. 访问Facebook Developers
https://developers.facebook.com/
# 2. 创建应用
- 选择类型: Consumer
# 3. 添加Facebook Login产品
# 4. 配置
- Valid OAuth Redirect URIs: http://localhost:3000/auth/facebook/callback
# 5. 获取
- App ID
- App Secret4.2 实现 Facebook 登录
typescript
// auth/facebook.strategy.ts
import passport from 'passport';
import { Strategy as FacebookStrategy } from 'passport-facebook';
passport.use(
new FacebookStrategy(
{
clientID: process.env.FACEBOOK_APP_ID!,
clientSecret: process.env.FACEBOOK_APP_SECRET!,
callbackURL: '/auth/facebook/callback',
profileFields: ['id', 'emails', 'name', 'picture']
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ facebookId: profile.id });
if (!user) {
user = await User.create({
facebookId: profile.id,
email: profile.emails?.[0]?.value,
name: `${profile.name?.givenName} ${profile.name?.familyName}`,
avatar: profile.photos?.[0]?.value,
provider: 'facebook'
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);typescript
// NextAuth.js配置
import FacebookProvider from 'next-auth/providers/facebook';
export default NextAuth({
providers: [
FacebookProvider({
clientId: process.env.FACEBOOK_ID!,
clientSecret: process.env.FACEBOOK_SECRET!
})
]
});5. 微信登录集成
5.1 微信开放平台配置
bash
# 1. 访问微信开放平台
https://open.weixin.qq.com/
# 2. 创建网站应用
# 3. 配置
- 授权回调域: yourdomain.com
# 4. 获取
- AppID
- AppSecret5.2 实现微信登录
typescript
// auth/wechat.strategy.ts
import passport from 'passport';
import { Strategy as WeChatStrategy } from 'passport-wechat';
passport.use(
new WeChatStrategy(
{
appID: process.env.WECHAT_APP_ID!,
appSecret: process.env.WECHAT_APP_SECRET!,
callbackURL: '/auth/wechat/callback',
scope: 'snsapi_userinfo',
state: 'state'
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ wechatId: profile.id });
if (!user) {
user = await User.create({
wechatId: profile.id,
nickname: profile.nickname,
avatar: profile.headimgurl,
provider: 'wechat'
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);6. 多平台登录统一处理
6.1 用户模型设计
typescript
// models/User.ts
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
// 本地认证
email: String,
passwordHash: String,
// 第三方认证
googleId: String,
githubId: String,
facebookId: String,
wechatId: String,
// 通用信息
name: String,
username: String,
avatar: String,
// 元数据
provider: {
type: String,
enum: ['local', 'google', 'github', 'facebook', 'wechat']
},
// OAuth令牌
accessToken: String,
refreshToken: String,
createdAt: {
type: Date,
default: Date.now
}
});
// 根据provider查找用户
userSchema.statics.findByProvider = function(provider: string, providerId: string) {
const query: any = {};
switch (provider) {
case 'google':
query.googleId = providerId;
break;
case 'github':
query.githubId = providerId;
break;
case 'facebook':
query.facebookId = providerId;
break;
case 'wechat':
query.wechatId = providerId;
break;
}
return this.findOne(query);
};
export const User = mongoose.model('User', userSchema);6.2 统一的认证处理
typescript
// services/auth.service.ts
interface OAuthProfile {
provider: string;
id: string;
email?: string;
name?: string;
avatar?: string;
accessToken: string;
refreshToken?: string;
}
export class AuthService {
static async handleOAuthLogin(profile: OAuthProfile) {
// 查找现有用户
let user = await User.findByProvider(profile.provider, profile.id);
if (!user) {
// 检查邮箱是否已存在
if (profile.email) {
user = await User.findOne({ email: profile.email });
if (user) {
// 关联第三方账号
await this.linkProvider(user, profile);
} else {
// 创建新用户
user = await this.createUser(profile);
}
} else {
// 没有邮箱,直接创建
user = await this.createUser(profile);
}
} else {
// 更新令牌
await this.updateTokens(user, profile);
}
return user;
}
static async createUser(profile: OAuthProfile) {
const userData: any = {
provider: profile.provider,
email: profile.email,
name: profile.name,
avatar: profile.avatar,
accessToken: profile.accessToken,
refreshToken: profile.refreshToken
};
// 设置provider ID
userData[`${profile.provider}Id`] = profile.id;
return User.create(userData);
}
static async linkProvider(user: any, profile: OAuthProfile) {
user[`${profile.provider}Id`] = profile.id;
user.accessToken = profile.accessToken;
user.refreshToken = profile.refreshToken;
await user.save();
return user;
}
static async updateTokens(user: any, profile: OAuthProfile) {
user.accessToken = profile.accessToken;
user.refreshToken = profile.refreshToken;
await user.save();
return user;
}
}6.3 统一的登录组件
tsx
// SocialLogin.tsx
import { signIn } from 'next-auth/react';
const providers = [
{
id: 'google',
name: 'Google',
icon: '/icons/google.svg',
color: 'bg-white border-gray-300'
},
{
id: 'github',
name: 'GitHub',
icon: '/icons/github.svg',
color: 'bg-gray-900 text-white'
},
{
id: 'facebook',
name: 'Facebook',
icon: '/icons/facebook.svg',
color: 'bg-blue-600 text-white'
}
];
export function SocialLogin() {
return (
<div className="space-y-3">
{providers.map((provider) => (
<button
key={provider.id}
onClick={() => signIn(provider.id, { callbackUrl: '/dashboard' })}
className={`w-full flex items-center justify-center gap-3 px-4 py-2 rounded-lg ${provider.color}`}
>
<img src={provider.icon} alt={provider.name} className="w-5 h-5" />
<span>使用 {provider.name} 登录</span>
</button>
))}
</div>
);
}7. 账号绑定与解绑
7.1 绑定第三方账号
typescript
// routes/account.ts
import express from 'express';
import passport from 'passport';
const router = express.Router();
// 绑定Google账号
router.get('/link/google',
ensureAuthenticated,
passport.authorize('google', {
scope: ['profile', 'email']
})
);
router.get('/link/google/callback',
passport.authorize('google', { failureRedirect: '/settings' }),
async (req: any, res) => {
const user = req.user;
const account = req.account;
// 关联账号
user.googleId = account.id;
await user.save();
res.redirect('/settings?linked=google');
}
);
// 解绑账号
router.post('/unlink/:provider',
ensureAuthenticated,
async (req: any, res) => {
const { provider } = req.params;
const user = req.user;
// 检查是否至少保留一种登录方式
const loginMethods = [
user.googleId,
user.githubId,
user.facebookId,
user.passwordHash
].filter(Boolean);
if (loginMethods.length <= 1) {
return res.status(400).json({
error: '必须至少保留一种登录方式'
});
}
// 解绑
user[`${provider}Id`] = undefined;
await user.save();
res.json({ success: true });
}
);
function ensureAuthenticated(req: any, res: any, next: any) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
export default router;7.2 账号绑定界面
tsx
// AccountSettings.tsx
import { useState, useEffect } from 'react';
export function AccountSettings() {
const [linkedAccounts, setLinkedAccounts] = useState({
google: false,
github: false,
facebook: false
});
useEffect(() => {
fetch('/api/user/linked-accounts')
.then(res => res.json())
.then(data => setLinkedAccounts(data));
}, []);
const handleLink = (provider: string) => {
window.location.href = `/account/link/${provider}`;
};
const handleUnlink = async (provider: string) => {
if (confirm(`确定要解绑 ${provider} 账号吗?`)) {
await fetch(`/account/unlink/${provider}`, {
method: 'POST'
});
setLinkedAccounts({
...linkedAccounts,
[provider]: false
});
}
};
return (
<div className="space-y-4">
<h2>已关联账号</h2>
{Object.entries(linkedAccounts).map(([provider, isLinked]) => (
<div key={provider} className="flex items-center justify-between p-4 border rounded">
<div className="flex items-center gap-3">
<img src={`/icons/${provider}.svg`} className="w-8 h-8" />
<span className="capitalize">{provider}</span>
</div>
{isLinked ? (
<button
onClick={() => handleUnlink(provider)}
className="px-4 py-2 text-red-600 border border-red-600 rounded"
>
解绑
</button>
) : (
<button
onClick={() => handleLink(provider)}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
绑定
</button>
)}
</div>
))}
</div>
);
}8. 安全最佳实践
8.1 State参数防CSRF
typescript
// 生成随机state
const state = crypto.randomUUID();
req.session.oauthState = state;
// 验证state
if (req.query.state !== req.session.oauthState) {
throw new Error('Invalid state parameter');
}8.2 存储OAuth令牌
typescript
// ❌ 不要存储在前端
localStorage.setItem('oauth_token', token);
// ✅ 存储在服务器端
user.accessToken = encryptToken(token);
user.refreshToken = encryptToken(refreshToken);
await user.save();
// 加密令牌
function encryptToken(token: string): string {
const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY!);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}8.3 验证邮箱唯一性
typescript
async function handleOAuthCallback(profile: any) {
// 检查邮箱是否已被其他用户使用
const existingUser = await User.findOne({
email: profile.email,
provider: { $ne: profile.provider }
});
if (existingUser) {
// 提示用户邮箱已被使用
throw new Error('该邮箱已被其他账号使用');
}
// 继续处理...
}9. 错误处理
typescript
// 统一错误处理
router.get('/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
failureMessage: true
}),
(req, res) => {
res.redirect('/dashboard');
},
(err: any, req: any, res: any, next: any) => {
console.error('OAuth Error:', err);
let errorMessage = '登录失败';
if (err.message.includes('email')) {
errorMessage = '无法获取邮箱信息';
} else if (err.message.includes('token')) {
errorMessage = '令牌获取失败';
}
res.redirect(`/login?error=${encodeURIComponent(errorMessage)}`);
}
);10. 测试
typescript
// oauth.test.ts
describe('OAuth Login', () => {
it('should create user on first login', async () => {
const profile = {
provider: 'google',
id: 'google123',
email: 'user@example.com',
name: 'Test User'
};
const user = await AuthService.handleOAuthLogin(profile);
expect(user.googleId).toBe('google123');
expect(user.email).toBe('user@example.com');
});
it('should link account if email exists', async () => {
// 创建本地用户
const localUser = await User.create({
email: 'user@example.com',
passwordHash: 'hashed',
provider: 'local'
});
// 使用相同邮箱的Google登录
const profile = {
provider: 'google',
id: 'google123',
email: 'user@example.com'
};
const user = await AuthService.handleOAuthLogin(profile);
expect(user.id).toBe(localUser.id);
expect(user.googleId).toBe('google123');
});
});11. 总结
第三方登录集成的关键要点:
- 选择合适的平台: 根据目标用户选择主流平台
- 统一处理: 使用统一的用户模型和认证流程
- 安全存储: OAuth令牌加密存储在服务器端
- 账号关联: 支持多个第三方账号绑定到同一用户
- 错误处理: 优雅处理各种错误情况
- 用户体验: 提供清晰的UI和反馈
通过正确实施第三方登录,可以显著降低用户注册门槛,提高用户转化率。