feat: oauth demo

v3
veypi 3 months ago
parent 959e390126
commit a5339aa589

@ -14,7 +14,7 @@ import (
"github.com/veypi/OneAuth/api/token"
"github.com/veypi/OneAuth/api/user"
"github.com/vyes/vigo"
"github.com/vyes/vigo/middlewares/common"
"github.com/vyes/vigo/contrib/common"
)
var Router = vigo.NewRouter().UseAfter(common.JsonResponse, common.JsonErrorResponse)

@ -9,9 +9,9 @@ package OneAuth
import (
"embed"
"github.com/veypi/OneAuth/api"
"github.com/vyes/vigo"
"github.com/vyes/vigo/middlewares/vyes"
"github.com/veypi/vyes-ui"
"github.com/vyes/vigo"
"github.com/vyes/vigo/contrib/vyes"
)
var Router = vigo.NewRouter()

@ -12,8 +12,8 @@ import (
"time"
"github.com/google/uuid"
"github.com/vyes/vigo/contrib/dbmodels"
"github.com/vyes/vigo/logv"
"github.com/vyes/vigo/middlewares/dbmodels"
"gorm.io/gorm"
)

@ -0,0 +1,242 @@
# OAuth 服务器数据库设计
本文档介绍了基于您现有用户管理系统的OAuth 2.0服务器数据库设计。
## 核心表结构
### 1. OAuth 客户端相关
#### `oauth_clients` - OAuth客户端表
存储注册到系统的OAuth客户端应用信息。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| client_id | varchar(255) | 客户端ID唯一 |
| client_secret | varchar(255) | 客户端密钥(加密存储) |
| client_name | varchar(255) | 客户端应用名称 |
| client_uri | varchar(500) | 客户端主页 |
| logo_uri | varchar(500) | 客户端Logo |
| redirect_uris | text | 重定向URI列表JSON格式 |
| response_types | varchar(255) | 支持的响应类型 |
| grant_types | varchar(255) | 支持的授权类型 |
| scope | text | 授权范围 |
| is_public | boolean | 是否为公开客户端 |
| is_active | boolean | 是否激活 |
| owner_id | varchar(32) | 客户端拥有者ID |
#### `oauth_scopes` - OAuth授权范围表
定义可用的授权范围。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| name | varchar(100) | 范围名称(唯一) |
| display_name | varchar(100) | 显示名称 |
| description | text | 范围描述 |
| is_default | boolean | 是否为默认范围 |
| is_system | boolean | 是否为系统范围 |
#### `oauth_client_scopes` - 客户端授权范围关联表
定义客户端可以请求的授权范围。
| 字段 | 类型 | 说明 |
|-----|------|------|
| client_id | varchar(32) | 客户端ID |
| scope_id | varchar(32) | 范围ID |
### 2. OAuth 授权流程相关
#### `oauth_authorization_codes` - 授权码表
存储授权码流程中的临时授权码。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| code | varchar(255) | 授权码(唯一) |
| client_id | varchar(32) | 客户端ID |
| user_id | varchar(32) | 用户ID |
| redirect_uri | varchar(500) | 重定向URI |
| scope | text | 授权范围 |
| code_challenge | varchar(255) | PKCE代码挑战 |
| code_challenge_method | varchar(50) | PKCE挑战方法 |
| expires_at | timestamp | 过期时间 |
| used | boolean | 是否已使用 |
#### `oauth_access_tokens` - 访问令牌表
存储颁发的访问令牌。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| token | varchar(500) | 访问令牌(唯一) |
| client_id | varchar(32) | 客户端ID |
| user_id | varchar(32) | 用户ID |
| scope | text | 授权范围 |
| expires_at | timestamp | 过期时间 |
| revoked | boolean | 是否已撤销 |
#### `oauth_refresh_tokens` - 刷新令牌表
存储刷新令牌,用于获取新的访问令牌。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| token | varchar(500) | 刷新令牌(唯一) |
| access_token_id | varchar(32) | 关联的访问令牌ID |
| client_id | varchar(32) | 客户端ID |
| user_id | varchar(32) | 用户ID |
| scope | text | 授权范围 |
| expires_at | timestamp | 过期时间 |
| revoked | boolean | 是否已撤销 |
#### `oauth_user_consents` - 用户授权同意表
记录用户对客户端的授权同意。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 用户ID |
| client_id | varchar(32) | 客户端ID |
| scope | text | 授权范围 |
| consent_at | timestamp | 同意时间 |
| expires_at | timestamp | 同意过期时间 |
### 3. 第三方OAuth登录相关
#### `oauth_providers` - 第三方OAuth提供商表
配置第三方OAuth提供商如GitHub, Google等
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| name | varchar(100) | 提供商名称(唯一) |
| display_name | varchar(100) | 显示名称 |
| client_id | varchar(255) | 客户端ID |
| client_secret | varchar(255) | 客户端密钥 |
| auth_url | varchar(500) | 授权URL |
| token_url | varchar(500) | 令牌URL |
| user_info_url | varchar(500) | 用户信息URL |
| scope | text | 默认授权范围 |
| is_active | boolean | 是否激活 |
#### `oauth_accounts` - 用户OAuth账户表
存储用户通过第三方OAuth登录的账户信息。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 本系统用户ID |
| provider_id | varchar(32) | 提供商ID |
| provider_user_id | varchar(255) | 提供商用户ID |
| email | varchar(255) | 邮箱 |
| username | varchar(255) | 用户名 |
| nickname | varchar(255) | 昵称 |
| avatar | varchar(500) | 头像URL |
| access_token | text | 访问令牌 |
| refresh_token | text | 刷新令牌 |
| expires_at | timestamp | 令牌过期时间 |
### 4. 用户会话和令牌管理
#### `user_sessions` - 用户会话表
管理用户登录会话。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 用户ID |
| session_id | varchar(255) | 会话ID唯一 |
| ip_address | varchar(45) | IP地址 |
| user_agent | text | 用户代理 |
| expires_at | timestamp | 过期时间 |
| is_active | boolean | 是否激活 |
| last_activity | timestamp | 最后活动时间 |
#### `user_tokens` - 用户令牌表
管理用户的API令牌等。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 用户ID |
| token_type | varchar(50) | 令牌类型 |
| token | varchar(500) | 令牌值(唯一) |
| name | varchar(100) | 令牌名称 |
| description | text | 令牌描述 |
| scope | text | 授权范围 |
| expires_at | timestamp | 过期时间 |
| last_used_at | timestamp | 最后使用时间 |
| is_active | boolean | 是否激活 |
## 关系说明
### 用户与OAuth的关系
- 一个用户可以拥有多个OAuth客户端`users.id` -> `oauth_clients.owner_id`
- 一个用户可以授权多个客户端(`users.id` -> `oauth_user_consents.user_id`
- 一个用户可以关联多个第三方账户(`users.id` -> `oauth_accounts.user_id`
### OAuth授权流程关系
- 授权码关联客户端和用户(`oauth_authorization_codes.client_id` -> `oauth_clients.id`
- 访问令牌关联刷新令牌(`oauth_refresh_tokens.access_token_id` -> `oauth_access_tokens.id`
### 权限控制
- 基于现有的RBAC系统为OAuth相关操作定义权限
- 管理员可以管理所有OAuth客户端
- 普通用户只能管理自己的OAuth客户端
## 预定义数据
### 默认授权范围
- `profile`: 基本资料访问
- `email`: 邮箱地址访问
- `phone`: 手机号码访问
- `read`: 读取权限
- `write`: 写入权限
- `admin`: 管理员权限
### 预配置的第三方提供商
- GitHub
- Google
- 微信
- 钉钉
### OAuth相关权限
- `oauth.client.create`: 创建OAuth客户端
- `oauth.client.read`: 查看OAuth客户端
- `oauth.client.update`: 更新OAuth客户端
- `oauth.client.delete`: 删除OAuth客户端
- `oauth.token.manage`: 管理OAuth令牌
- `oauth.scope.manage`: 管理OAuth作用域
- `oauth.provider.manage`: 管理OAuth提供商
## 安全考虑
1. **令牌存储**: 所有敏感令牌都应该进行哈希或加密存储
2. **HTTPS强制**: 所有OAuth端点必须使用HTTPS
3. **PKCE支持**: 支持PKCE以增强安全性
4. **令牌过期**: 设置合理的令牌过期时间
5. **审计日志**: 记录所有OAuth相关操作
## 使用示例
### 初始化OAuth数据
```go
import "vyes_cli/oauth"
// 在数据库迁移后调用
err := oauth.InitializeOAuthData(db)
if err != nil {
log.Fatal("Failed to initialize OAuth data:", err)
}
```
### 创建测试客户端
```go
client, err := oauth.CreateDefaultOAuthClient(db, adminUserID)
if err != nil {
log.Fatal("Failed to create default OAuth client:", err)
}
```
这个设计基于OAuth 2.0 RFC标准同时考虑了您现有的用户管理系统架构可以无缝集成到您的项目中。

@ -0,0 +1,134 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"github.com/veypi/OneAuth/cfg"
"github.com/vyes/vigo"
"gorm.io/gorm"
"time"
)
// AuthorizeRequest 授权请求参数
type AuthorizeRequest struct {
ResponseType string `form:"response_type" binding:"required"`
ClientID string `form:"client_id" binding:"required"`
RedirectURI string `form:"redirect_uri" binding:"required"`
Scope string `form:"scope"`
State string `form:"state"`
CodeChallenge string `form:"code_challenge"`
CodeChallengeMethod string `form:"code_challenge_method"`
}
// AuthorizeResponse 授权响应
type AuthorizeResponse struct {
Code string `json:"code,omitempty"`
State string `json:"state,omitempty"`
RedirectURI string `json:"redirect_uri"`
Error string `json:"error,omitempty"`
ErrorDesc string `json:"error_description,omitempty"`
}
// handleAuthorize 处理OAuth授权请求
func handleAuthorize(x *vigo.X) error {
args := &AuthorizeRequest{}
if err := x.Parse(args); err != nil {
return vigo.NewError("参数解析失败").WithError(err).WithCode(400)
}
db := cfg.DB()
// 1. 验证响应类型
if args.ResponseType != ResponseTypeCode {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorUnsupportedResponseType, "不支持的响应类型", args.State)
return x.JSON(&AuthorizeResponse{
Error: "unsupported_response_type",
ErrorDesc: "不支持的响应类型",
RedirectURI: errorURI,
})
}
// 2. 验证客户端
var client OAuthClient
if err := db.Where("client_id = ? AND is_active = ?", args.ClientID, true).First(&client).Error; err != nil {
if err == gorm.ErrRecordNotFound {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorInvalidClient, "无效的客户端", args.State)
return x.JSON(&AuthorizeResponse{
Error: "invalid_client",
ErrorDesc: "无效的客户端",
RedirectURI: errorURI,
})
}
return vigo.NewError("数据库查询失败").WithError(err).WithCode(500)
}
// 3. 验证重定向URI
if !client.IsRedirectURIValid(args.RedirectURI) {
return vigo.NewError("无效的重定向URI").WithCode(400)
}
// 4. 验证作用域
if args.Scope != "" && !client.HasScope(args.Scope) {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorInvalidScope, "无效的授权范围", args.State)
return x.JSON(&AuthorizeResponse{
Error: "invalid_scope",
ErrorDesc: "无效的授权范围",
RedirectURI: errorURI,
})
}
// TODO: 在实际应用中,这里应该:
// 1. 检查用户是否已登录
// 2. 显示授权同意页面
// 3. 用户同意后生成授权码
// 为了演示,这里假设用户已登录且同意授权
// 假设当前用户ID (实际应从session或JWT token中获取)
userID := "demo-user-id"
// 5. 生成授权码
code, err := generateRandomString(32)
if err != nil {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorServerError, "授权码生成失败", args.State)
return x.JSON(&AuthorizeResponse{
Error: "server_error",
ErrorDesc: "授权码生成失败",
RedirectURI: errorURI,
})
}
// 6. 创建授权码记录
authCode := &OAuthAuthorizationCode{
Code: code,
ClientID: client.ID,
UserID: userID,
RedirectURI: args.RedirectURI,
Scope: args.Scope,
CodeChallenge: args.CodeChallenge,
CodeChallengeMethod: args.CodeChallengeMethod,
ExpiresAt: time.Now().Add(DefaultAuthorizationCodeExpiry),
Used: false,
}
if err := db.Create(authCode).Error; err != nil {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorServerError, "授权码保存失败", args.State)
return x.JSON(&AuthorizeResponse{
Error: "server_error",
ErrorDesc: "授权码保存失败",
RedirectURI: errorURI,
})
}
// 7. 构建成功重定向URI
redirectURI := BuildRedirectURI(args.RedirectURI, authCode.Code, args.State)
return x.JSON(&AuthorizeResponse{
Code: authCode.Code,
State: args.State,
RedirectURI: redirectURI,
})
}

@ -0,0 +1,159 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import "time"
// OAuth 2.0 相关常量
const (
// Grant Types
GrantTypeAuthorizationCode = "authorization_code"
GrantTypeRefreshToken = "refresh_token"
GrantTypeClientCredentials = "client_credentials"
GrantTypePassword = "password"
GrantTypeImplicit = "implicit"
// Response Types
ResponseTypeCode = "code"
ResponseTypeToken = "token"
// Token Types
TokenTypeBearer = "Bearer"
// PKCE Challenge Methods
CodeChallengeMethodPlain = "plain"
CodeChallengeMethodS256 = "S256"
// Default Scopes
ScopeRead = "read"
ScopeWrite = "write"
ScopeProfile = "profile"
ScopeEmail = "email"
ScopePhone = "phone"
ScopeAdmin = "admin"
// Token 生存时间
DefaultAuthorizationCodeExpiry = 10 * time.Minute // 授权码10分钟过期
DefaultAccessTokenExpiry = 1 * time.Hour // 访问令牌1小时过期
DefaultRefreshTokenExpiry = 30 * 24 * time.Hour // 刷新令牌30天过期
DefaultSessionExpiry = 24 * time.Hour // 会话24小时过期
// Error Codes (RFC 6749)
ErrorInvalidRequest = "invalid_request"
ErrorInvalidClient = "invalid_client"
ErrorInvalidGrant = "invalid_grant"
ErrorUnauthorizedClient = "unauthorized_client"
ErrorUnsupportedGrantType = "unsupported_grant_type"
ErrorInvalidScope = "invalid_scope"
ErrorAccessDenied = "access_denied"
ErrorUnsupportedResponseType = "unsupported_response_type"
ErrorServerError = "server_error"
ErrorTemporarilyUnavailable = "temporarily_unavailable"
// PKCE Error Codes (RFC 7636)
ErrorInvalidGrant2 = "invalid_grant"
// Token 类型
UserTokenTypeAPI = "api" // API 令牌
UserTokenTypeSession = "session" // 会话令牌
UserTokenTypePersonal = "personal" // 个人访问令牌
)
// 默认作用域定义
var DefaultScopes = []struct {
Name string
DisplayName string
Description string
IsDefault bool
IsSystem bool
}{
{
Name: ScopeProfile,
DisplayName: "基本资料",
Description: "访问您的基本资料信息,如用户名、昵称等",
IsDefault: true,
IsSystem: true,
},
{
Name: ScopeEmail,
DisplayName: "邮箱地址",
Description: "访问您的邮箱地址",
IsDefault: false,
IsSystem: true,
},
{
Name: ScopePhone,
DisplayName: "手机号码",
Description: "访问您的手机号码",
IsDefault: false,
IsSystem: true,
},
{
Name: ScopeRead,
DisplayName: "读取权限",
Description: "读取您的数据",
IsDefault: true,
IsSystem: false,
},
{
Name: ScopeWrite,
DisplayName: "写入权限",
Description: "修改您的数据",
IsDefault: false,
IsSystem: false,
},
{
Name: ScopeAdmin,
DisplayName: "管理员权限",
Description: "完全的管理员权限",
IsDefault: false,
IsSystem: true,
},
}
// 预定义的第三方OAuth提供商
var DefaultOAuthProviders = []struct {
Name string
DisplayName string
AuthURL string
TokenURL string
UserInfoURL string
Scope string
}{
{
Name: "github",
DisplayName: "GitHub",
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
UserInfoURL: "https://api.github.com/user",
Scope: "user:email",
},
{
Name: "google",
DisplayName: "Google",
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
UserInfoURL: "https://www.googleapis.com/oauth2/v2/userinfo",
Scope: "openid profile email",
},
{
Name: "wechat",
DisplayName: "微信",
AuthURL: "https://open.weixin.qq.com/connect/oauth2/authorize",
TokenURL: "https://api.weixin.qq.com/sns/oauth2/access_token",
UserInfoURL: "https://api.weixin.qq.com/sns/userinfo",
Scope: "snsapi_userinfo",
},
{
Name: "dingtalk",
DisplayName: "钉钉",
AuthURL: "https://oapi.dingtalk.com/connect/oauth2/sns_authorize",
TokenURL: "https://oapi.dingtalk.com/sns/gettoken",
UserInfoURL: "https://oapi.dingtalk.com/sns/getuserinfo",
Scope: "snsapi_login",
},
}

@ -0,0 +1,210 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"github.com/vyes/vigo"
"gorm.io/gorm"
)
var Router = vigo.NewRouter()
func init() {
// OAuth 授权端点
var _ = Router.Get("/authorize", `OAuth授权端点 - 获取授权码`, AuthorizeRequest{}, handleAuthorize)
// OAuth 令牌端点
var _ = Router.Post("/token", `OAuth令牌端点 - 用授权码换取令牌或刷新令牌`, TokenRequest{}, handleToken)
// OAuth 撤销端点
var _ = Router.Post("/revoke", `OAuth撤销端点 - 撤销访问令牌或刷新令牌`, RevokeRequest{}, handleRevoke)
}
// InitializeOAuthData 初始化OAuth相关的基础数据
func InitializeOAuthData(db *gorm.DB) error {
// 1. 创建默认的OAuth作用域
if err := createDefaultScopes(db); err != nil {
return err
}
// 2. 创建默认的第三方OAuth提供商
if err := createDefaultProviders(db); err != nil {
return err
}
// 3. 创建默认权限
if err := createDefaultPermissions(db); err != nil {
return err
}
return nil
}
func createDefaultScopes(db *gorm.DB) error {
for _, scopeData := range DefaultScopes {
scope := &OAuthScope{
Name: scopeData.Name,
DisplayName: scopeData.DisplayName,
Description: scopeData.Description,
IsDefault: scopeData.IsDefault,
IsSystem: scopeData.IsSystem,
}
// 如果不存在则创建
var existingScope OAuthScope
result := db.Where("name = ?", scope.Name).First(&existingScope)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(scope).Error; err != nil {
return err
}
}
}
return nil
}
func createDefaultProviders(db *gorm.DB) error {
for _, providerData := range DefaultOAuthProviders {
provider := &OAuthProvider{
Name: providerData.Name,
DisplayName: providerData.DisplayName,
AuthURL: providerData.AuthURL,
TokenURL: providerData.TokenURL,
UserInfoURL: providerData.UserInfoURL,
Scope: providerData.Scope,
IsActive: false, // 默认不激活需要配置ClientID和ClientSecret后激活
}
// 如果不存在则创建
var existingProvider OAuthProvider
result := db.Where("name = ?", provider.Name).First(&existingProvider)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(provider).Error; err != nil {
return err
}
}
}
return nil
}
func createDefaultPermissions(db *gorm.DB) error {
oauthPermissions := []struct {
Name string
DisplayName string
Description string
Resource string
Action string
IsSystem bool
}{
{
Name: "oauth.client.create",
DisplayName: "创建OAuth客户端",
Description: "允许创建新的OAuth客户端应用",
Resource: "oauth_client",
Action: "create",
IsSystem: true,
},
{
Name: "oauth.client.read",
DisplayName: "查看OAuth客户端",
Description: "允许查看OAuth客户端信息",
Resource: "oauth_client",
Action: "read",
IsSystem: true,
},
{
Name: "oauth.client.update",
DisplayName: "更新OAuth客户端",
Description: "允许更新OAuth客户端信息",
Resource: "oauth_client",
Action: "update",
IsSystem: true,
},
{
Name: "oauth.client.delete",
DisplayName: "删除OAuth客户端",
Description: "允许删除OAuth客户端",
Resource: "oauth_client",
Action: "delete",
IsSystem: true,
},
{
Name: "oauth.token.manage",
DisplayName: "管理OAuth令牌",
Description: "允许管理用户的OAuth令牌",
Resource: "oauth_token",
Action: "manage",
IsSystem: true,
},
{
Name: "oauth.scope.manage",
DisplayName: "管理OAuth作用域",
Description: "允许管理OAuth作用域",
Resource: "oauth_scope",
Action: "manage",
IsSystem: true,
},
{
Name: "oauth.provider.manage",
DisplayName: "管理OAuth提供商",
Description: "允许管理第三方OAuth提供商",
Resource: "oauth_provider",
Action: "manage",
IsSystem: true,
},
}
for _, permData := range oauthPermissions {
permission := &Permission{
Name: permData.Name,
DisplayName: permData.DisplayName,
Description: permData.Description,
Resource: permData.Resource,
Action: permData.Action,
IsSystem: permData.IsSystem,
}
// 如果不存在则创建
var existingPerm Permission
result := db.Where("name = ?", permission.Name).First(&existingPerm)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(permission).Error; err != nil {
return err
}
}
}
return nil
}
// CreateDefaultOAuthClient 创建默认的OAuth客户端用于测试
func CreateDefaultOAuthClient(db *gorm.DB, ownerID string) (*OAuthClient, error) {
client := &OAuthClient{
ClientID: "default-client-id",
ClientSecret: "default-client-secret", // 实际使用时应该使用加密存储
ClientName: "Default Test Client",
ClientURI: "http://localhost:3000",
RedirectURIs: `["http://localhost:3000/callback", "http://localhost:8080/callback"]`,
ResponseTypes: "code",
GrantTypes: "authorization_code,refresh_token",
Scope: "profile read write",
IsPublic: false,
IsActive: true,
OwnerID: ownerID,
}
// 检查是否已存在
var existingClient OAuthClient
result := db.Where("client_id = ?", client.ClientID).First(&existingClient)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(client).Error; err != nil {
return nil, err
}
return client, nil
}
return &existingClient, nil
}

@ -0,0 +1,65 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"github.com/veypi/OneAuth/cfg"
"github.com/vyes/vigo"
)
// RevokeRequest 撤销令牌请求参数
type RevokeRequest struct {
Token string `form:"token" binding:"required"`
TokenTypeHint string `form:"token_type_hint"` // access_token 或 refresh_token
ClientID string `form:"client_id"`
ClientSecret string `form:"client_secret"`
}
// RevokeResponse 撤销令牌响应
type RevokeResponse struct {
Message string `json:"message"`
Success bool `json:"success"`
}
// handleRevoke 处理OAuth撤销请求
func handleRevoke(x *vigo.X) error {
args := &RevokeRequest{}
if err := x.Parse(args); err != nil {
return vigo.NewError("参数解析失败").WithError(err).WithCode(400)
}
if args.Token == "" {
return vigo.NewError("令牌不能为空").WithCode(400)
}
db := cfg.DB()
// 根据OAuth 2.0规范,撤销令牌应该是幂等操作
// 即使令牌不存在也应该返回成功,这是为了防止信息泄露
var revoked = false
// 尝试撤销访问令牌
result := db.Model(&OAuthAccessToken{}).Where("token = ?", args.Token).Update("revoked", true)
if result.Error == nil && result.RowsAffected > 0 {
revoked = true
}
// 如果没有找到访问令牌,尝试撤销刷新令牌
if !revoked {
result = db.Model(&OAuthRefreshToken{}).Where("token = ?", args.Token).Update("revoked", true)
if result.Error == nil && result.RowsAffected > 0 {
revoked = true
}
}
// 根据OAuth 2.0规范,即使令牌不存在也返回成功
// 这样可以防止攻击者通过响应差异推断令牌是否存在
return x.JSON(&RevokeResponse{
Message: "令牌撤销成功",
Success: true,
})
}

@ -0,0 +1,294 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"github.com/veypi/OneAuth/cfg"
"github.com/vyes/vigo"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"time"
)
// TokenRequest 令牌请求参数
type TokenRequest struct {
GrantType string `form:"grant_type" binding:"required"`
Code string `form:"code"`
RedirectURI string `form:"redirect_uri"`
ClientID string `form:"client_id"`
ClientSecret string `form:"client_secret"`
RefreshToken string `form:"refresh_token"`
CodeVerifier string `form:"code_verifier"`
Username string `form:"username"` // for password grant
Password string `form:"password"` // for password grant
Scope string `form:"scope"` // for password grant
}
// TokenResponse 令牌响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
}
// handleToken 处理OAuth令牌请求
func handleToken(x *vigo.X) error {
args := &TokenRequest{}
if err := x.Parse(args); err != nil {
return vigo.NewError("参数解析失败").WithError(err).WithCode(400)
}
db := cfg.DB()
switch args.GrantType {
case GrantTypeAuthorizationCode:
return handleAuthorizationCodeGrant(db, x, args)
case GrantTypeRefreshToken:
return handleRefreshTokenGrant(db, x, args)
case GrantTypePassword:
return handlePasswordGrant(db, x, args)
default:
return vigo.NewError("不支持的授权类型").WithCode(400)
}
}
// handleAuthorizationCodeGrant 处理授权码授权类型
func handleAuthorizationCodeGrant(db *gorm.DB, x *vigo.X, args *TokenRequest) error {
// 1. 验证授权码
var authCode OAuthAuthorizationCode
if err := db.Where("code = ? AND used = ?", args.Code, false).First(&authCode).Error; err != nil {
return vigo.NewError("无效的授权码").WithCode(400)
}
// 2. 检查授权码是否过期
if authCode.IsExpired() {
return vigo.NewError("授权码已过期").WithCode(400)
}
// 3. 验证客户端
var client OAuthClient
if err := db.Where("id = ? AND client_id = ?", authCode.ClientID, args.ClientID).First(&client).Error; err != nil {
return vigo.NewError("无效的客户端").WithCode(400)
}
// 4. 验证客户端密钥(对于机密客户端)
if !client.IsPublic && client.ClientSecret != args.ClientSecret {
return vigo.NewError("无效的客户端凭据").WithCode(400)
}
// 5. 验证重定向URI
if authCode.RedirectURI != args.RedirectURI {
return vigo.NewError("重定向URI不匹配").WithCode(400)
}
// 6. 验证PKCE如果使用
if authCode.CodeChallenge != "" {
if err := validatePKCE(authCode.CodeChallenge, authCode.CodeChallengeMethod, args.CodeVerifier); err != nil {
return vigo.NewError("PKCE验证失败").WithError(err).WithCode(400)
}
}
// 7. 标记授权码为已使用
if err := db.Model(&authCode).Update("used", true).Error; err != nil {
return vigo.NewError("授权码更新失败").WithError(err).WithCode(500)
}
// 8. 生成访问令牌
accessToken, err := generateAccessToken(db, &client, authCode.UserID, authCode.Scope)
if err != nil {
return vigo.NewError("访问令牌生成失败").WithError(err).WithCode(500)
}
// 9. 生成刷新令牌
refreshToken, err := generateRefreshToken(db, accessToken, &client, authCode.UserID, authCode.Scope)
if err != nil {
return vigo.NewError("刷新令牌生成失败").WithError(err).WithCode(500)
}
return x.JSON(&TokenResponse{
AccessToken: accessToken.Token,
TokenType: TokenTypeBearer,
ExpiresIn: int64(DefaultAccessTokenExpiry.Seconds()),
RefreshToken: refreshToken.Token,
Scope: authCode.Scope,
})
}
// handleRefreshTokenGrant 处理刷新令牌授权类型
func handleRefreshTokenGrant(db *gorm.DB, x *vigo.X, args *TokenRequest) error {
// 1. 验证刷新令牌
var refreshToken OAuthRefreshToken
if err := db.Where("token = ? AND revoked = ?", args.RefreshToken, false).First(&refreshToken).Error; err != nil {
return vigo.NewError("无效的刷新令牌").WithCode(400)
}
// 2. 检查刷新令牌是否过期
if refreshToken.IsExpired() {
return vigo.NewError("刷新令牌已过期").WithCode(400)
}
// 3. 验证客户端
var client OAuthClient
if err := db.Where("id = ? AND client_id = ?", refreshToken.ClientID, args.ClientID).First(&client).Error; err != nil {
return vigo.NewError("无效的客户端").WithCode(400)
}
// 4. 撤销旧的访问令牌
if err := db.Model(&OAuthAccessToken{}).Where("id = ?", refreshToken.AccessTokenID).Update("revoked", true).Error; err != nil {
return vigo.NewError("旧令牌撤销失败").WithError(err).WithCode(500)
}
// 5. 生成新的访问令牌
accessToken, err := generateAccessToken(db, &client, refreshToken.UserID, refreshToken.Scope)
if err != nil {
return vigo.NewError("访问令牌生成失败").WithError(err).WithCode(500)
}
// 6. 更新刷新令牌关联
if err := db.Model(&refreshToken).Update("access_token_id", accessToken.ID).Error; err != nil {
return vigo.NewError("刷新令牌更新失败").WithError(err).WithCode(500)
}
return x.JSON(&TokenResponse{
AccessToken: accessToken.Token,
TokenType: TokenTypeBearer,
ExpiresIn: int64(DefaultAccessTokenExpiry.Seconds()),
RefreshToken: refreshToken.Token,
Scope: refreshToken.Scope,
})
}
// handlePasswordGrant 处理密码授权类型
func handlePasswordGrant(db *gorm.DB, x *vigo.X, args *TokenRequest) error {
// 1. 验证必要参数
if args.Username == "" || args.Password == "" {
return vigo.NewError("用户名和密码不能为空").WithCode(400)
}
// 2. 验证客户端
var client OAuthClient
if err := db.Where("client_id = ?", args.ClientID).First(&client).Error; err != nil {
return vigo.NewError("无效的客户端").WithCode(400)
}
// 3. 验证客户端密钥(对于机密客户端)
if !client.IsPublic && client.ClientSecret != args.ClientSecret {
return vigo.NewError("无效的客户端凭据").WithCode(400)
}
// 4. 验证用户凭据
var user User
if err := db.Where("username = ?", args.Username).First(&user).Error; err != nil {
return vigo.NewError("用户名或密码错误").WithCode(400)
}
// 5. 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(args.Password)); err != nil {
return vigo.NewError("用户名或密码错误").WithCode(400)
}
// 6. 处理权限范围
scope := args.Scope
if scope == "" {
// scope = DefaultScope // 默认权限范围
}
// 7. 生成访问令牌
accessToken, err := generateAccessToken(db, &client, user.ID, scope)
if err != nil {
return vigo.NewError("访问令牌生成失败").WithError(err).WithCode(500)
}
// 8. 生成刷新令牌
refreshToken, err := generateRefreshToken(db, accessToken, &client, user.ID, scope)
if err != nil {
return vigo.NewError("刷新令牌生成失败").WithError(err).WithCode(500)
}
return x.JSON(&TokenResponse{
AccessToken: accessToken.Token,
TokenType: TokenTypeBearer,
ExpiresIn: int64(DefaultAccessTokenExpiry.Seconds()),
RefreshToken: refreshToken.Token,
Scope: scope,
})
}
// 辅助函数
func generateAccessToken(db *gorm.DB, client *OAuthClient, userID, scope string) (*OAuthAccessToken, error) {
token, err := generateRandomString(64)
if err != nil {
return nil, err
}
accessToken := &OAuthAccessToken{
Token: token,
ClientID: client.ID,
UserID: userID,
Scope: scope,
ExpiresAt: time.Now().Add(DefaultAccessTokenExpiry),
Revoked: false,
}
if err := db.Create(accessToken).Error; err != nil {
return nil, err
}
return accessToken, nil
}
func generateRefreshToken(db *gorm.DB, accessToken *OAuthAccessToken, client *OAuthClient, userID, scope string) (*OAuthRefreshToken, error) {
token, err := generateRandomString(64)
if err != nil {
return nil, err
}
refreshToken := &OAuthRefreshToken{
Token: token,
AccessTokenID: accessToken.ID,
ClientID: client.ID,
UserID: userID,
Scope: scope,
ExpiresAt: time.Now().Add(DefaultRefreshTokenExpiry),
Revoked: false,
}
if err := db.Create(refreshToken).Error; err != nil {
return nil, err
}
return refreshToken, nil
}
func validatePKCE(codeChallenge, method, codeVerifier string) error {
if codeVerifier == "" {
return fmt.Errorf("code verifier required")
}
switch method {
case CodeChallengeMethodPlain:
if codeChallenge != codeVerifier {
return fmt.Errorf("invalid code verifier")
}
case CodeChallengeMethodS256:
h := sha256.Sum256([]byte(codeVerifier))
expected := base64.RawURLEncoding.EncodeToString(h[:])
if codeChallenge != expected {
return fmt.Errorf("invalid code verifier")
}
default:
return fmt.Errorf("unsupported code challenge method")
}
return nil
}

@ -0,0 +1,400 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"time"
"github.com/veypi/OneAuth/models"
"gorm.io/gorm"
)
// User 用户表
type User struct {
models.BaseModel
Username string `json:"username" gorm:"uniqueIndex;not null;size:50;comment:用户名""`
Email string `json:"email" gorm:"uniqueIndex;size:100;comment:邮箱地址"`
Phone string `json:"phone" gorm:"uniqueIndex;size:20;comment:手机号码"`
PasswordHash string `json:"-" gorm:"not null;size:255;comment:密码哈希"`
Nickname string `json:"nickname" gorm:"size:50;comment:昵称"`
Avatar string `json:"avatar" gorm:"size:255;comment:头像URL"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
IsSuperuser bool `json:"is_superuser" gorm:"default:false;comment:是否为超级用户"`
LastLoginAt *time.Time `json:"last_login_at" gorm:"comment:最后登录时间"`
EmailVerified bool `json:"email_verified" gorm:"default:false;comment:邮箱是否已验证"`
PhoneVerified bool `json:"phone_verified" gorm:"default:false;comment:手机是否已验证"`
TwoFactorAuth bool `json:"two_factor_auth" gorm:"default:false;comment:是否启用双因素认证"`
Locale string `json:"locale" gorm:"size:10;default:zh-CN;comment:语言偏好"`
Timezone string `json:"timezone" gorm:"size:50;default:Asia/Shanghai;comment:时区"`
Bio string `json:"bio" gorm:"type:text;comment:个人简介"`
// 关联关系
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
UserRoles []UserRole `json:"-"`
OAuthAccounts []OAuthAccount `json:"oauth_accounts"`
Tokens []UserToken `json:"-"`
MetaData []byte `json:"meta_data" gorm:"type:jsonb;comment:用户元数据"` // 存储用户自定义的元数据
}
// Role 角色表
type Role struct {
models.BaseModel
Name string `json:"name" gorm:"uniqueIndex;not null;size:50;comment:角色名称" validate:"required"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
Description string `json:"description" gorm:"type:text;comment:角色描述"`
IsSystem bool `json:"is_system" gorm:"default:false;comment:是否为系统角色"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
// 关联关系
Users []User `json:"-" gorm:"many2many:user_roles;"`
UserRoles []UserRole `json:"-"`
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
}
// Permission 权限表
type Permission struct {
models.BaseModel
Name string `json:"name" gorm:"uniqueIndex;not null;size:100;comment:权限名称" validate:"required"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
Description string `json:"description" gorm:"type:text;comment:权限描述"`
Resource string `json:"resource" gorm:"not null;size:50;comment:资源名称" validate:"required"`
Action string `json:"action" gorm:"not null;size:50;comment:操作类型" validate:"required"`
IsSystem bool `json:"is_system" gorm:"default:false;comment:是否为系统权限"`
// 关联关系
Roles []Role `json:"-" gorm:"many2many:role_permissions;"`
}
// UserRole 用户角色关联表
type UserRole struct {
UserID string `json:"user_id" gorm:"primaryKey;type:varchar(32);comment:用户ID"`
RoleID string `json:"role_id" gorm:"primaryKey;type:varchar(32);comment:角色ID"`
GrantedBy string `json:"granted_by" gorm:"type:varchar(32);comment:授权人ID"`
GrantedAt time.Time `json:"granted_at" gorm:"autoCreateTime;comment:授权时间"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:过期时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
Role *Role `json:"role" gorm:"foreignKey:RoleID"`
GrantedByUser *User `json:"granted_by_user" gorm:"foreignKey:GrantedBy"`
}
// RolePermission 角色权限关联表
type RolePermission struct {
RoleID string `json:"role_id" gorm:"primaryKey;type:varchar(32);comment:角色ID"`
PermissionID string `json:"permission_id" gorm:"primaryKey;type:varchar(32);comment:权限ID"`
GrantedBy string `json:"granted_by" gorm:"type:varchar(32);comment:授权人ID"`
GrantedAt time.Time `json:"granted_at" gorm:"autoCreateTime;comment:授权时间"`
// 关联关系
Role *Role `json:"role" gorm:"foreignKey:RoleID"`
Permission *Permission `json:"permission" gorm:"foreignKey:PermissionID"`
GrantedByUser *User `json:"granted_by_user" gorm:"foreignKey:GrantedBy"`
}
// UserLoginLog 用户登录日志表
type UserLoginLog struct {
models.BaseModel
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
IPAddress string `json:"ip_address" gorm:"size:45;comment:IP地址"`
UserAgent string `json:"user_agent" gorm:"type:text;comment:用户代理"`
LoginAt time.Time `json:"login_at" gorm:"autoCreateTime;comment:登录时间"`
Success bool `json:"success" gorm:"default:true;comment:是否成功"`
FailReason string `json:"fail_reason" gorm:"size:255;comment:失败原因"`
Location string `json:"location" gorm:"size:100;comment:地理位置"` // 地理位置
// 关联关系
User User `json:"user" gorm:"foreignKey:UserID"`
}
// GORM Hooks
func (u *User) BeforeCreate(tx *gorm.DB) error {
if err := u.BaseModel.BeforeCreate(tx); err != nil {
return err
}
if u.Locale == "" {
u.Locale = "zh-CN"
}
if u.Timezone == "" {
u.Timezone = "Asia/Shanghai"
}
return nil
}
// 用户方法
func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles {
if role.Name == roleName {
return true
}
}
return false
}
func (u *User) HasPermission(resource, action string) bool {
for _, role := range u.Roles {
for _, permission := range role.Permissions {
if permission.Resource == resource && permission.Action == action {
return true
}
}
}
return false
}
func (u *User) GetPermissions() []Permission {
var permissions []Permission
permissionMap := make(map[string]bool)
for _, role := range u.Roles {
for _, permission := range role.Permissions {
if !permissionMap[permission.ID] {
permissions = append(permissions, permission)
permissionMap[permission.ID] = true
}
}
}
return permissions
}
// 角色方法
func (r *Role) HasPermission(resource, action string) bool {
for _, permission := range r.Permissions {
if permission.Resource == resource && permission.Action == action {
return true
}
}
return false
}
// ===== OAuth 服务器相关模型 =====
// OAuthClient OAuth客户端表
type OAuthClient struct {
models.BaseModel
ClientID string `json:"client_id" gorm:"uniqueIndex;not null;size:255;comment:客户端ID"`
ClientSecret string `json:"-" gorm:"not null;size:255;comment:客户端密钥"`
ClientName string `json:"client_name" gorm:"not null;size:255;comment:客户端名称"`
ClientURI string `json:"client_uri" gorm:"size:500;comment:客户端主页"`
LogoURI string `json:"logo_uri" gorm:"size:500;comment:客户端Logo"`
TermsOfServiceURI string `json:"tos_uri" gorm:"size:500;comment:服务条款URL"`
PolicyURI string `json:"policy_uri" gorm:"size:500;comment:隐私政策URL"`
RedirectURIs string `json:"redirect_uris" gorm:"type:text;comment:重定向URI列表,JSON格式"`
ResponseTypes string `json:"response_types" gorm:"size:255;default:'code';comment:响应类型"`
GrantTypes string `json:"grant_types" gorm:"size:255;default:'authorization_code,refresh_token';comment:授权类型"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
Contacts string `json:"contacts" gorm:"type:text;comment:联系人邮箱,JSON格式"`
IsPublic bool `json:"is_public" gorm:"default:false;comment:是否为公开客户端"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
OwnerID string `json:"owner_id" gorm:"type:varchar(32);comment:客户端拥有者ID"`
// 关联关系
Owner *User `json:"owner" gorm:"foreignKey:OwnerID"`
AuthorizationCodes []OAuthAuthorizationCode `json:"-"`
AccessTokens []OAuthAccessToken `json:"-"`
RefreshTokens []OAuthRefreshToken `json:"-"`
}
// OAuthAuthorizationCode 授权码表
type OAuthAuthorizationCode struct {
models.BaseModel
Code string `json:"code" gorm:"uniqueIndex;not null;size:255;comment:授权码"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
RedirectURI string `json:"redirect_uri" gorm:"not null;size:500;comment:重定向URI"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
CodeChallenge string `json:"code_challenge" gorm:"size:255;comment:PKCE代码挑战"`
CodeChallengeMethod string `json:"code_challenge_method" gorm:"size:50;comment:PKCE挑战方法"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
Used bool `json:"used" gorm:"default:false;comment:是否已使用"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
User *User `json:"user" gorm:"foreignKey:UserID"`
}
// OAuthAccessToken 访问令牌表
type OAuthAccessToken struct {
models.BaseModel
Token string `json:"token" gorm:"uniqueIndex;not null;size:500;comment:访问令牌"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
Revoked bool `json:"revoked" gorm:"default:false;comment:是否已撤销"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
User *User `json:"user" gorm:"foreignKey:UserID"`
RefreshToken *OAuthRefreshToken `json:"refresh_token"`
}
// OAuthRefreshToken 刷新令牌表
type OAuthRefreshToken struct {
models.BaseModel
Token string `json:"token" gorm:"uniqueIndex;not null;size:500;comment:刷新令牌"`
AccessTokenID string `json:"access_token_id" gorm:"type:varchar(32);uniqueIndex;comment:访问令牌ID"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
Revoked bool `json:"revoked" gorm:"default:false;comment:是否已撤销"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
User *User `json:"user" gorm:"foreignKey:UserID"`
AccessToken *OAuthAccessToken `json:"access_token" gorm:"foreignKey:AccessTokenID"`
}
// OAuthScope OAuth授权范围表
type OAuthScope struct {
models.BaseModel
Name string `json:"name" gorm:"uniqueIndex;not null;size:100;comment:范围名称"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
Description string `json:"description" gorm:"type:text;comment:范围描述"`
IsDefault bool `json:"is_default" gorm:"default:false;comment:是否为默认范围"`
IsSystem bool `json:"is_system" gorm:"default:false;comment:是否为系统范围"`
}
// OAuthClientScope 客户端授权范围关联表
type OAuthClientScope struct {
ClientID string `json:"client_id" gorm:"primaryKey;type:varchar(32);comment:客户端ID"`
ScopeID string `json:"scope_id" gorm:"primaryKey;type:varchar(32);comment:范围ID"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
Scope *OAuthScope `json:"scope" gorm:"foreignKey:ScopeID"`
}
// OAuthProvider 第三方OAuth提供商表用于OAuth客户端模式
type OAuthProvider struct {
models.BaseModel
Name string `json:"name" gorm:"uniqueIndex;not null;size:100;comment:提供商名称"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
ClientID string `json:"client_id" gorm:"not null;size:255;comment:客户端ID"`
ClientSecret string `json:"-" gorm:"not null;size:255;comment:客户端密钥"`
AuthURL string `json:"auth_url" gorm:"not null;size:500;comment:授权URL"`
TokenURL string `json:"token_url" gorm:"not null;size:500;comment:令牌URL"`
UserInfoURL string `json:"user_info_url" gorm:"size:500;comment:用户信息URL"`
Scope string `json:"scope" gorm:"type:text;comment:默认授权范围"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
// 关联关系
OAuthAccounts []OAuthAccount `json:"oauth_accounts"`
}
// OAuthAccount 用户OAuth账户表第三方登录
type OAuthAccount struct {
models.BaseModel
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
ProviderID string `json:"provider_id" gorm:"not null;type:varchar(32);index;comment:提供商ID"`
ProviderUserID string `json:"provider_user_id" gorm:"not null;size:255;comment:提供商用户ID"`
Email string `json:"email" gorm:"size:255;comment:邮箱"`
Username string `json:"username" gorm:"size:255;comment:用户名"`
Nickname string `json:"nickname" gorm:"size:255;comment:昵称"`
Avatar string `json:"avatar" gorm:"size:500;comment:头像URL"`
AccessToken string `json:"-" gorm:"type:text;comment:访问令牌"`
RefreshToken string `json:"-" gorm:"type:text;comment:刷新令牌"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:令牌过期时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
Provider *OAuthProvider `json:"provider" gorm:"foreignKey:ProviderID"`
}
// UserToken 用户令牌表API令牌等
type UserToken struct {
models.BaseModel
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
TokenType string `json:"token_type" gorm:"not null;size:50;comment:令牌类型"` // api, session, etc.
Token string `json:"token" gorm:"uniqueIndex;not null;size:500;comment:令牌值"`
Name string `json:"name" gorm:"size:100;comment:令牌名称"`
Description string `json:"description" gorm:"type:text;comment:令牌描述"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:过期时间"`
LastUsedAt *time.Time `json:"last_used_at" gorm:"comment:最后使用时间"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
}
// UserSession 用户会话表
type UserSession struct {
models.BaseModel
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
SessionID string `json:"session_id" gorm:"uniqueIndex;not null;size:255;comment:会话ID"`
IPAddress string `json:"ip_address" gorm:"size:45;comment:IP地址"`
UserAgent string `json:"user_agent" gorm:"type:text;comment:用户代理"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
LastActivity time.Time `json:"last_activity" gorm:"comment:最后活动时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
}
// OAuthUserConsent 用户授权同意表
type OAuthUserConsent struct {
models.BaseModel
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ConsentAt time.Time `json:"consent_at" gorm:"autoCreateTime;comment:同意时间"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:同意过期时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
}
// ===== OAuth 模型方法 =====
// OAuthClient 方法
func (c *OAuthClient) IsRedirectURIValid(uri string) bool {
// 这里应该解析 RedirectURIs JSON 并验证
// 简化实现,实际使用时需要完善
return true
}
func (c *OAuthClient) HasScope(scope string) bool {
// 这里应该解析 Scope 并检查
// 简化实现,实际使用时需要完善
return true
}
// OAuthAuthorizationCode 方法
func (code *OAuthAuthorizationCode) IsExpired() bool {
return time.Now().After(code.ExpiresAt)
}
// OAuthAccessToken 方法
func (token *OAuthAccessToken) IsExpired() bool {
return time.Now().After(token.ExpiresAt)
}
func (token *OAuthAccessToken) IsValid() bool {
return !token.Revoked && !token.IsExpired()
}
// OAuthRefreshToken 方法
func (token *OAuthRefreshToken) IsExpired() bool {
return time.Now().After(token.ExpiresAt)
}
func (token *OAuthRefreshToken) IsValid() bool {
return !token.Revoked && !token.IsExpired()
}
// UserSession 方法
func (s *UserSession) IsExpired() bool {
return time.Now().After(s.ExpiresAt)
}
func (s *UserSession) IsValid() bool {
return s.IsActive && !s.IsExpired()
}

@ -0,0 +1,49 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"crypto/rand"
"encoding/hex"
"net/url"
)
// generateRandomString 生成指定长度的随机字符串
func generateRandomString(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes)[:length], nil
}
// BuildRedirectURI 构建成功重定向URI
func BuildRedirectURI(baseURI, code, state string) string {
u, _ := url.Parse(baseURI)
q := u.Query()
q.Set("code", code)
if state != "" {
q.Set("state", state)
}
u.RawQuery = q.Encode()
return u.String()
}
// BuildErrorRedirectURI 构建错误重定向URI
func BuildErrorRedirectURI(baseURI, errorCode, errorDesc, state string) string {
u, _ := url.Parse(baseURI)
q := u.Query()
q.Set("error", errorCode)
if errorDesc != "" {
q.Set("error_description", errorDesc)
}
if state != "" {
q.Set("state", state)
}
u.RawQuery = q.Encode()
return u.String()
}
Loading…
Cancel
Save