通用的 OAuth2 服务端和客户端实现,支持多种授权模式。
pkg/oauth2/
├── common/ # 公共类型和常量
│ └── types.go # 公共类型定义
├── server/ # OAuth2 服务端实现
│ ├── types.go # 服务端类型定义
│ ├── token.go # Token 生成和验证
│ └── handler.go # HTTP 处理器
├── client/ # OAuth2 客户端实现
│ ├── types.go # 客户端类型定义
│ └── client.go # 客户端实现
└── README.md # 文档
package main
import (
"time"
"sync"
"sun-panel-micro-store/pkg/oauth2/server"
)
// 实现第三方应用存储接口
type ThirdAppStoreImpl struct {
apps map[string]ThirdAppInfo
}
func (s *ThirdAppStoreImpl) GetByClientID(clientID string) (server.ThirdAppInfo, error) {
app, ok := s.apps[clientID]
if !ok {
return nil, errors.New("not found")
}
return app, nil
}
func (s *ThirdAppStoreImpl) GetByClientIDAndSecret(clientID, clientSecret string) (server.ThirdAppInfo, error) {
app, ok := s.apps[clientID]
if !ok || app.GetClientSecret() != clientSecret {
return nil, errors.New("invalid credentials")
}
return app, nil
}
// 实现其他存储接口...
package main
import (
"github.com/gin-gonic/gin"
"sun-panel-micro-store/pkg/oauth2/server"
)
func main() {
// 创建配置
config := &server.OAuthConfig{
AccessTokenExpireTime: 7200, // 2小时
RefreshTokenExpireTime: 604800, // 7天
AuthCodeExpireTime: 600, // 10分钟
EnableSSOLogout: true,
}
// 创建 handler
handler := server.NewOAuthHandler(config)
// 设置存储实现
handler.SetStores(
&ThirdAppStoreImpl{},
&UserStoreImpl{},
&TokenStoreImpl{},
&AuthCodeStoreImpl{},
&RefreshTokenStoreImpl{},
)
// 注册路由
r := gin.Default()
handler.RegisterRoutes(r.Group("/api"))
r.Run(":8080")
}
package main
import (
"context"
"fmt"
"sun-panel-micro-store/pkg/oauth2/client"
)
func main() {
config := &client.Config{
AuthServerURL: "http://localhost:8080",
APIServerURL: "http://localhost:8080",
ClientID: "your_client_id",
ClientSecret: "your_client_secret",
RedirectURI: "http://localhost:3000/callback",
Timeout: 30,
}
oauthClient, err := client.NewOAuth2Client(config)
if err != nil {
panic(err)
}
// 获取授权 URL
authURL := oauthClient.GetAuthorizationURL(config.RedirectURI, "random_state")
fmt.Println("授权 URL:", authURL)
}
func handleCallback(code string) {
ctx := context.Background()
tokenResp, err := oauthClient.GetAccessTokenByCode(ctx, code)
if err != nil {
panic(err)
}
fmt.Printf("Access Token: %s\n", tokenResp.AccessToken)
fmt.Printf("Refresh Token: %s\n", tokenResp.RefreshToken)
fmt.Printf("Expires In: %d\n", tokenResp.ExpiresIn)
}
func getClientToken() {
ctx := context.Background()
tokenResp, err := oauthClient.GetClientCredentialsToken(ctx)
if err != nil {
panic(err)
}
fmt.Printf("Access Token: %s\n", tokenResp.AccessToken)
}
func callAPI(accessToken string) {
apiClient := client.NewAPIClient("http://localhost:8080", 30)
ctx := context.Background()
userInfo, err := apiClient.GetUserInfo(ctx, accessToken)
if err != nil {
panic(err)
}
fmt.Printf("User: %s (%s)\n", userInfo.Name, userInfo.Email)
}
| 端点 | 方法 | 描述 |
|---|---|---|
/oauth2/v1/authorize | GET | 授权端点 |
/oauth2/v1/token | POST | Token 端点(授权码模式、密码模式) |
/oauth2/v1/clientCredentials/token | POST | 客户端凭证模式 Token 端点 |
/oauth2/v1/sso/logout | POST | 单点登出 |
1. 用户访问授权 URL
2. 用户授权后获得 code
3. 使用 code 换取 access_token
1. 用户提供用户名和密码
2. 直接获取 access_token
1. 客户端提供 client_id 和 client_secret
2. 获取 access_token(无用户上下文)
1. 使用 refresh_token 获取新的 access_token
type ThirdAppStore interface {
GetByClientID(clientID string) (ThirdAppInfo, error)
GetByClientIDAndSecret(clientID, clientSecret string) (ThirdAppInfo, error)
}
type UserStore interface {
GetByUsernameAndPassword(username, password string) (UserInfo, error)
GetByID(userID uint) (UserInfo, error)
}
type TokenStore interface {
SetAccessToken(token string, data AccessTokenData) error
GetAccessToken(token string) (AccessTokenData, error)
DeleteAccessToken(token string) error
}
type AuthCodeStore interface {
SetAuthCode(code string, data OAuthCodeData) error
GetAuthCode(code string) (OAuthCodeData, error)
DeleteAuthCode(code string) error
}
type RefreshTokenStore interface {
SetRefreshToken(token string, data RefreshTokenData) error
GetRefreshToken(token string) (RefreshTokenData, error)
DeleteRefreshToken(token string) error
}
可以使用 Redis、数据库或其他存储方式实现存储接口。
import "github.com/go-redis/redis/v8"
type RedisTokenStore struct {
client *redis.Client
}
func (s *RedisTokenStore) SetAccessToken(token string, data server.AccessTokenData) error {
ctx := context.Background()
jsonData, _ := json.Marshal(data)
return s.client.Set(ctx, "token:"+token, jsonData, 2*time.Hour).Err()
}
func (s *RedisTokenStore) GetAccessToken(token string) (server.AccessTokenData, error) {
ctx := context.Background()
data, err := s.client.Get(ctx, "token:"+token).Bytes()
if err != nil {
return server.AccessTokenData{}, err
}
var tokenData server.AccessTokenData
json.Unmarshal(data, &tokenData)
return tokenData, nil
}
func (s *RedisTokenStore) DeleteAccessToken(token string) error {
ctx := context.Background()
return s.client.Del(ctx, "token:"+token).Err()
}
MIT License