Vue3 + Go 实现第三方 OAuth 登录(GitHub & Microsoft)完整指南
在现代 Web 应用中,第三方登录已经成为用户体验的重要组成部分。本文将详细介绍如何在 Vue3 前端和 Go 后端项目中实现 GitHub 和 Microsoft OAuth 登录功能。
项目架构
- 前端: Vue3 + TypeScript + Arco Design
- 后端: Go + Gin + GORM
- 数据库: MySQL
- OAuth 提供商: GitHub、Microsoft
一、OAuth 登录流程原理
标准 OAuth 2.0 流程
用户 > 前端 > 后端重定向接口 > OAuth授权页面 > 用户授权 >
OAuth回调后端 > 后端获取用户信息 > 后端生成JWT >
后端重定向前端 > 前端保存token > 跳转首页
详细步骤:
- 用户点击第三方登录按钮
- 前端跳转到后端重定向接口
- 后端重定向到 OAuth 提供商授权页面
- 用户在 OAuth 页面授权
- OAuth 提供商回调后端并返回授权码
- 后端用授权码换取 access_token
- 后端用 access_token 获取用户信息
- 后端创建/更新用户,生成 JWT token
- 后端重定向到前端回调页面(携带 token)
- 前端保存 token,跳转到目标页面
二、后端实现
1. 数据库模型设计
go
type UserModel struct {
ID uint `gorm:"primaryKey"`
Username string
Nickname string
Email string
Avatar string
Password string
// 第三方登录ID(统一使用string类型兼容所有平台)
GithubID string `gorm:"index"`
MicrosoftID string `gorm:"index"`
Role int // 用户角色
SignStatus int // 登录方式标识
IP string
Addr string
CreatedAt time.Time
UpdatedAt time.Time
}
2. GitHub OAuth 实现
OAuth 配置
go
type GitHubOAuth struct{}
func (g GitHubOAuth) Config() *oauth2.Config {
return &oauth2.Config{
ClientID: config.GitHub.ClientID,
ClientSecret: config.GitHub.ClientSecret,
RedirectURL: config.GitHub.RedirectURL,
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}
}
用户信息结构
go
type GithubUserResponse struct {
ID int `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
}
重定向接口
go
func GithubOAuthRedirectView(c *gin.Context) {
config := GitHubOAuth{}.Config()
url := config.AuthCodeURL("state", oauth2.AccessTypeOnline)
c.Redirect(http.StatusFound, url)
}
回调处理
go
func GithubOAuthCallbackView(c *gin.Context) {
// 获取授权码
code := c.Query("code")
if code == "" {
c.JSON(400, gin.H{"error": "认证失败"})
return
}
// 交换access_token
config := GitHubOAuth{}.Config()
token, err := config.Exchange(c, code)
if err != nil {
c.JSON(400, gin.H{"error": "认证失败"})
return
}
// 获取用户信息
client := config.Client(c, token)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
c.JSON(400, gin.H{"error": "获取用户信息失败"})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var githubUser GithubUserResponse
if err := json.Unmarshal(body, &githubUser); err != nil {
c.JSON(400, gin.H{"error": "用户信息解析失败"})
return
}
// 获取用户IP和地理位置
userIP := c.ClientIP()
// 地理位置解析逻辑...
// 查找或创建用户
var user UserModel
err = db.Where("github_id = ?", strconv.Itoa(githubUser.ID)).First(&user).Error
if err != nil {
// 创建新用户
user = UserModel{
Username: githubUser.Login,
Nickname: githubUser.Name,
Avatar: githubUser.AvatarURL,
GithubID: strconv.Itoa(githubUser.ID),
Email: githubUser.Email,
IP: userIP,
Role: 2, // 普通用户
SignStatus: 1, // GitHub登录
}
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "创建用户失败"})
return
}
}
// 生成JWT token
accessToken := generateAccessToken(user)
refreshToken := generateRefreshToken(user)
// 重定向到前端回调页面
callbackURL := fmt.Sprintf("http://localhost:3000/login/github/callback?access_token=%s&refresh_token=%s",
accessToken, refreshToken)
c.Redirect(http.StatusFound, callbackURL)
}
3. Microsoft OAuth 实现
OAuth 配置
go
type MicrosoftOAuth struct{}
func (m MicrosoftOAuth) Config() *oauth2.Config {
return &oauth2.Config{
ClientID: config.Microsoft.ClientID,
ClientSecret: config.Microsoft.ClientSecret,
RedirectURL: config.Microsoft.RedirectURL,
Scopes: []string{"https://graph.microsoft.com/User.Read"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
},
}
}
用户信息结构
go
type MicrosoftUserResponse struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
GivenName string `json:"givenName"`
UserPrincipalName string `json:"userPrincipalName"`
Mail string `json:"mail"`
}
回调处理(关键差异)
go
func MicrosoftOAuthCallbackView(c *gin.Context) {
// 获取授权码(注意:使用Query而不是Param)
code := c.Query("code")
if code == "" {
c.JSON(400, gin.H{"error": "认证失败"})
return
}
// 交换token
config := MicrosoftOAuth{}.Config()
token, err := config.Exchange(c, code)
if err != nil {
c.JSON(400, gin.H{"error": "认证失败"})
return
}
// 获取用户信息(使用Microsoft Graph API)
client := config.Client(c, token)
resp, err := client.Get("https://graph.microsoft.com/v1.0/me")
if err != nil {
c.JSON(400, gin.H{"error": "获取用户信息失败"})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var microsoftUser MicrosoftUserResponse
if err := json.Unmarshal(body, µsoftUser); err != nil {
c.JSON(400, gin.H{"error": "用户信息解析失败"})
return
}
// 查找或创建用户
var user UserModel
err = db.Where("microsoft_id = ?", microsoftUser.ID).First(&user).Error
if err != nil {
user = UserModel{
Username: microsoftUser.UserPrincipalName,
Nickname: microsoftUser.DisplayName,
MicrosoftID: microsoftUser.ID,
Email: microsoftUser.Mail,
Role: 2, // 普通用户
SignStatus: 2, // Microsoft登录
}
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "创建用户失败"})
return
}
}
// 生成JWT并重定向
accessToken := generateAccessToken(user)
refreshToken := generateRefreshToken(user)
callbackURL := fmt.Sprintf("http://localhost:3000/login/microsoft/callback?access_token=%s&refresh_token=%s",
accessToken, refreshToken)
c.Redirect(http.StatusFound, callbackURL)
}
4. 路由配置
go
func InitOAuthRoutes(router *gin.RouterGroup) {
// GitHub OAuth
router.GET("auth/github", GithubOAuthRedirectView)
router.GET("auth/github/callback", GithubOAuthCallbackView)
// Microsoft OAuth
router.GET("auth/microsoft", MicrosoftOAuthRedirectView)
router.GET("auth/microsoft/callback", MicrosoftOAuthCallbackView)
}
三、前端实现
1. 登录表单组件
vue
<template>
<div class="login-form">
<div class="title">用户登录</div>
<template></template>
<template></template>
登录
<div class="oauth-login">
<div class="label">第三方登录</div>
<div class="icons">
<a>
<img src="/images/github.png" alt="GitHub登录">
</a>
<a>
<img src="/images/microsoft.svg" alt="Microsoft登录">
</a>
</div>
</div>
</div>
</template>
<style lang="scss">
.oauth-login {
margin-top: 20px;
.label {
text-align: center;
color: #666;
margin-bottom: 15px;
position: relative;
&::before, &::after {
content: "";
position: absolute;
top: 50%;
width: 30%;
height: 1px;
background-color: #ddd;
}
&::before { left: 0; }
&::after { right: 0; }
}
.icons {
display: flex;
justify-content: center;
gap: 20px;
img {
width: 40px;
height: 40px;
cursor: pointer;
transition: transform 0.2s;
&:hover {
transform: scale(1.1);
}
}
}
}
</style>
2. GitHub 回调页面
vue
<template>
<div class="oauth-callback">
<div class="loading-container">
<p>正在处理 GitHub 登录...</p>
</div>
</div>
</template>
<style lang="scss">
.oauth-callback {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
.loading-container {
text-align: center;
p {
margin-top: 16px;
color: #666;
}
}
}
</style>
3. Microsoft 回调页面
Microsoft 回调页面与 GitHub 类似,只需要修改相关的文案和标识符:
vue
<template>
<div class="oauth-callback">
<div class="loading-container">
<p>正在处理 Microsoft 登录...</p>
</div>
</div>
</template>
4. 路由配置
typescript
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue')
},
{
path: "/login/github/callback",
name: "github_callback",
component: () => import('@/views/login/github-callback.vue')
},
{
path: "/login/microsoft/callback",
name: "microsoft_callback",
component: () => import('@/views/login/microsoft-callback.vue')
},
// 其他路由...
]
})
export default router
5. 用户状态管理
typescript
import { defineStore } from 'pinia'
interface UserInfo {
access_token: string
refresh_token: string
user_id: number
nickname: string
avatar: string
role: number
}
export const useUserStore = defineStore('user', {
state(): { userInfo: UserInfo } {
return {
userInfo: {
access_token: "",
refresh_token: "",
user_id: 0,
nickname: "",
avatar: "",
role: 0
}
}
},
actions: {
setToken(accessToken: string, refreshToken: string) {
this.userInfo.access_token = accessToken
this.userInfo.refresh_token = refreshToken
// 解析JWT获取用户信息
const payload = this.parseJWT(accessToken)
if (payload) {
this.userInfo.user_id = payload.user_id
this.userInfo.nickname = payload.nickname
this.userInfo.avatar = payload.avatar
this.userInfo.role = payload.role
}
// 持久化存储
localStorage.setItem("userInfo", JSON.stringify(this.userInfo))
},
parseJWT(token: string) {
try {
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64).split('').map(c =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join('')
)
return JSON.parse(jsonPayload)
} catch (error) {
console.error('JWT解析失败:', error)
return null
}
},
logout() {
this.userInfo = {
access_token: "",
refresh_token: "",
user_id: 0,
nickname: "",
avatar: "",
role: 0
}
localStorage.removeItem("userInfo")
}
},
getters: {
isLoggedIn(): boolean {
return !!this.userInfo.access_token && this.userInfo.user_id > 0
}
}
})
四、OAuth 应用配置
1. GitHub OAuth 应用
- 访问 GitHub Developer Settings
- 点击 "New OAuth App"
- 填写应用信息:
- Application name: 你的应用名称
- Homepage URL:
http://localhost:3000(开发环境) - Authorization callback URL:
http://localhost:8000/api/auth/github/callback
- 获取 Client ID 和 Client Secret
2. Microsoft OAuth 应用
- 访问 Azure Portal
- 进入 Azure Active Directory → 应用注册
- 点击 "新注册"
- 填写应用信息:
- 名称: 你的应用名称
- 支持的帐户类型: 任何组织目录中的帐户和个人 Microsoft 帐户
- 配置身份验证:
- 平台: Web
- 重定向 URI:
http://localhost:8000/api/auth/microsoft/callback - 令牌配置: 勾选 "ID 令牌" 和 "访问令牌"
- 生成客户端密钥
五、常见问题与解决方案
1. redirect_uri 不匹配
错误信息: The provided value for the input parameter 'redirect_uri' is not valid
解决方案:
- 确保 OAuth 应用配置中的回调 URL 与后端代码完全一致
- 检查协议(http/https)、域名、端口、路径是否匹配
- 注意
localhost和127.0.0.1的区别
2. scope 权限配置
GitHub:
- 基本信息:
[]string{"user:email"} - 更多权限:
[]string{"user", "user:email"}
Microsoft:
- Graph API:
[]string{"https://graph.microsoft.com/User.Read"} - OpenID Connect:
[]string{"openid", "profile", "email"}
3. JSON 解析失败
确保结构体的 JSON 标签与 API 返回字段名匹配:
go
// GitHub API 返回字段
type GithubUser struct {
ID int `json:"id"` // 正确
AvatarURL string `json:"avatar_url"` // 注意下划线
}
// Microsoft Graph API 返回字段
type MicrosoftUser struct {
ID string `json:"id"` // 正确
DisplayName string `json:"displayName"` // 注意驼峰命名
}
4. 获取授权码方式
正确方式:
go
code := c.Query("code") // 使用 Query 获取 URL 参数
错误方式:
go
code := c.Param("code") // 错误:Param 用于路径参数
5. 频繁请求限制
开发阶段避免频繁测试:
- 清除浏览器缓存和 Cookie
- 在 OAuth 提供商处撤销应用授权
- 使用不同浏览器或无痕模式
- 等待一段时间后重试
6. 前端回调页面问题
确保后端重定向到前端页面而不是返回 JSON:
go
// 正确:重定向到前端
callbackURL := fmt.Sprintf("http://localhost:3000/login/github/callback?access_token=%s", token)
c.Redirect(http.StatusFound, callbackURL)
// 错误:返回 JSON
c.JSON(200, gin.H{"access_token": token})
六、安全考虑
1. HTTPS 使用
生产环境必须使用 HTTPS:
- OAuth 提供商要求 HTTPS 回调 URL
- 保护 token 传输安全
- 防止中间人攻击
2. State 参数验证
go
// 生成随机 state
state := generateRandomString(32)
// 存储到 session 或 Redis
session.Set("oauth_state", state)
// 回调时验证
callbackState := c.Query("state")
sessionState := session.Get("oauth_state")
if callbackState != sessionState {
c.JSON(400, gin.H{"error": "状态验证失败"})
return
}
3. Token 安全
- 设置合理的 JWT 过期时间(如 2 小时)
- 实现 Refresh Token 机制
- 敏感信息不存储在 JWT payload 中
- 使用强密钥签名 JWT
4. 输入验证
go
// 验证必要字段
if user.ID == "" || user.Email == "" {
c.JSON(400, gin.H{"error": "用户信息不完整"})
return
}
// 邮箱格式验证
if !isValidEmail(user.Email) {
c.JSON(400, gin.H{"error": "邮箱格式无效"})
return
}
5. 错误处理
不要在错误信息中暴露敏感信息:
go
// 好的做法
c.JSON(400, gin.H{"error": "认证失败"})
// 避免暴露详细错误
c.JSON(400, gin.H{"error": fmt.Sprintf("数据库错误: %v", err)})
七、部署注意事项
1. 环境变量配置
bash
# GitHub OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_REDIRECT_URL=https://yourdomain.com/api/auth/github/callback
# Microsoft OAuth
MICROSOFT_CLIENT_ID=your_microsoft_client_id
MICROSOFT_CLIENT_SECRET=your_microsoft_client_secret
MICROSOFT_REDIRECT_URL=https://yourdomain.com/api/auth/microsoft/callback
# JWT 密钥
JWT_SECRET=your_jwt_secret_key
2. 域名配置
生产环境需要更新:
- OAuth 应用的回调 URL
- 前端环境变量中的 API 地址
- 后端重定向的前端地址
3. CORS 配置
go
func CORSMiddleware() gin.HandlerFunc {
return gin.HandlerFunc(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "https://yourdomain.com")
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
}
八、总结
本文详细介绍了 Vue3 + Go 实现第三方 OAuth 登录的完整方案,包括:
核心要点:
- 理解 OAuth 流程:掌握授权码模式的完整流程
- 后端实现:正确处理重定向、回调、用户信息获取和 JWT 生成
- 前端实现:简洁的登录界面和回调页面处理
- 配置管理:OAuth 应用的正确配置和环境变量管理
- 安全考虑:State 验证、HTTPS、Token 安全等
- 错误处理:完善的错误处理和用户友好的提示
技术收获:
- OAuth 2.0 标准流程的实际应用
- 前后端分离架构下的认证方案
- JWT Token 的生成和验证
- Vue3 Composition API 的实践
- Go Gin 框架的 HTTP 处理
通过这套完整的实现方案,用户可以便捷地使用 GitHub 或 Microsoft 账号登录应用,大大提升用户体验。在实际项目中,还可以根据需要扩展支持更多 OAuth 提供商,如 Google、微信、QQ 等。