Files
user-system/internal/auth/sso.go
long-agent 765a50b7d4 fix: 生产安全修复 + Go SDK + CAS SSO框架
安全修复:
- CRITICAL: SSO重定向URL注入漏洞 - 修复redirect_uri白名单验证
- HIGH: SSO ClientSecret未验证 - 使用crypto/subtle.ConstantTimeCompare验证
- HIGH: 邮件验证码熵值过低(3字节) - 提升到6字节(48位熵)
- HIGH: 短信验证码熵值过低(4字节) - 提升到6字节
- HIGH: Goroutine使用已取消上下文 - auth_email.go使用独立context+超时
- HIGH: SQL LIKE查询注入风险 - permission/role仓库使用escapeLikePattern

新功能:
- Go SDK: sdk/go/user-management/ 完整SDK实现
- CAS SSO框架: internal/auth/cas.go CAS协议支持

其他:
- L1Cache实例问题修复 - AuthMiddleware共享l1Cache
- 设备指纹XSS防护 - 内存存储替代localStorage
- 响应格式协议中间件
- 导出无界查询修复
2026-04-03 17:38:31 +08:00

339 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"sync"
"time"
)
const (
// MaxSessions 最大 session 数量限制
MaxSessions = 10000
// CleanupInterval 清理间隔
CleanupInterval = 5 * time.Minute
)
// SSOOAuth2Config SSO OAuth2 配置
type SSOOAuth2Config struct {
ClientID string
ClientSecret string
RedirectURI string
Scope string
}
// SSOProvider SSO 提供者接口
type SSOProvider interface {
// Authorize 处理授权请求
Authorize(ctx context.Context, req *SSOAuthorizeRequest) (*SSOAuthorizeResponse, error)
// Introspect 验证 access token
Introspect(ctx context.Context, token string) (*SSOTokenInfo, error)
// Revoke 撤销 token
Revoke(ctx context.Context, token string) error
}
// SSOAuthorizeRequest 授权请求
type SSOAuthorizeRequest struct {
ClientID string
RedirectURI string
ResponseType string // "code" 或 "token"
Scope string
State string
UserID int64
}
// SSOAuthorizeResponse 授权响应
type SSOAuthorizeResponse struct {
Code string // 授权码authorization_code 模式)
State string
}
// SSOTokenInfo Token 信息
type SSOTokenInfo struct {
Active bool
UserID int64
Username string
ExpiresAt time.Time
Scope string
ClientID string
}
// SSOSession SSO Session
type SSOSession struct {
SessionID string
UserID int64
Username string
ClientID string
CreatedAt time.Time
ExpiresAt time.Time
Scope string
}
// SSOManager SSO 管理器
type SSOManager struct {
mu sync.RWMutex
sessions map[string]*SSOSession
}
// NewSSOManager 创建 SSO 管理器
func NewSSOManager() *SSOManager {
return &SSOManager{
sessions: make(map[string]*SSOSession),
}
}
// StartCleanup 启动后台清理 goroutine
func (m *SSOManager) StartCleanup(ctx context.Context) {
go func() {
ticker := time.NewTicker(CleanupInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
m.CleanupExpired()
}
}
}()
}
// GenerateAuthorizationCode 生成授权码
func (m *SSOManager) GenerateAuthorizationCode(clientID, redirectURI, scope string, userID int64, username string) (string, error) {
code, err := generateSecureToken(32)
if err != nil {
return "", err
}
sessionID, err := generateSecureToken(16)
if err != nil {
return "", err
}
session := &SSOSession{
SessionID: sessionID,
UserID: userID,
Username: username,
ClientID: clientID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(10 * time.Minute), // 授权码 10 分钟有效期
Scope: scope,
}
m.mu.Lock()
// 检查并清理过期 session如果超过限制则淘汰最旧的
if len(m.sessions) >= MaxSessions {
m.cleanupExpiredLocked()
// 如果仍然满,淘汰最早的
if len(m.sessions) >= MaxSessions {
m.evictOldest()
}
}
m.sessions[code] = session
m.mu.Unlock()
return code, nil
}
// ValidateAuthorizationCode 验证授权码
func (m *SSOManager) ValidateAuthorizationCode(code string) (*SSOSession, error) {
m.mu.Lock()
defer m.mu.Unlock()
session, ok := m.sessions[code]
if !ok {
return nil, errors.New("invalid authorization code")
}
if time.Now().After(session.ExpiresAt) {
delete(m.sessions, code)
return nil, errors.New("authorization code expired")
}
// 使用后删除
delete(m.sessions, code)
return session, nil
}
// GenerateAccessToken 生成访问令牌
func (m *SSOManager) GenerateAccessToken(clientID string, session *SSOSession) (string, time.Time, error) {
token, err := generateSecureToken(32)
if err != nil {
return "", time.Time{}, err
}
expiresAt := time.Now().Add(2 * time.Hour) // Access token 2 小时有效期
accessSession := &SSOSession{
SessionID: token,
UserID: session.UserID,
Username: session.Username,
ClientID: clientID,
CreatedAt: time.Now(),
ExpiresAt: expiresAt,
Scope: session.Scope,
}
m.mu.Lock()
// 检查并清理过期 session如果超过限制则淘汰最旧的
if len(m.sessions) >= MaxSessions {
m.cleanupExpiredLocked()
if len(m.sessions) >= MaxSessions {
m.evictOldest()
}
}
m.sessions[token] = accessSession
m.mu.Unlock()
return token, expiresAt, nil
}
// IntrospectToken 验证 token
func (m *SSOManager) IntrospectToken(token string) (*SSOTokenInfo, error) {
m.mu.RLock()
session, ok := m.sessions[token]
if !ok {
m.mu.RUnlock()
return &SSOTokenInfo{Active: false}, nil
}
if time.Now().After(session.ExpiresAt) {
m.mu.RUnlock()
m.mu.Lock()
delete(m.sessions, token)
m.mu.Unlock()
return &SSOTokenInfo{Active: false}, nil
}
m.mu.RUnlock()
return &SSOTokenInfo{
Active: true,
UserID: session.UserID,
Username: session.Username,
ExpiresAt: session.ExpiresAt,
Scope: session.Scope,
ClientID: session.ClientID,
}, nil
}
// RevokeToken 撤销 token
func (m *SSOManager) RevokeToken(token string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.sessions, token)
return nil
}
// CleanupExpired 清理过期的 session
func (m *SSOManager) CleanupExpired() {
m.mu.Lock()
defer m.mu.Unlock()
m.cleanupExpiredLocked()
}
// cleanupExpiredLocked 内部清理方法(假设已持有锁)
func (m *SSOManager) cleanupExpiredLocked() {
now := time.Now()
for key, session := range m.sessions {
if now.After(session.ExpiresAt) {
delete(m.sessions, key)
}
}
}
// evictOldest 淘汰最早的 session假设已持有锁
func (m *SSOManager) evictOldest() {
if len(m.sessions) == 0 {
return
}
var oldestKey string
var oldestTime time.Time
for key, session := range m.sessions {
if oldestTime.IsZero() || session.CreatedAt.Before(oldestTime) {
oldestTime = session.CreatedAt
oldestKey = key
}
}
if oldestKey != "" {
delete(m.sessions, oldestKey)
}
}
// SessionCount 返回当前 session 数量(用于监控)
func (m *SSOManager) SessionCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.sessions)
}
// generateSecureToken 生成安全随机 token
func generateSecureToken(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate secure token: %w", err)
}
return base64.URLEncoding.EncodeToString(bytes)[:length], nil
}
// SSOClient SSO 客户端配置存储
type SSOClient struct {
ClientID string
ClientSecret string
Name string
RedirectURIs []string
}
// SSOClientsStore SSO 客户端存储接口
type SSOClientsStore interface {
GetByClientID(clientID string) (*SSOClient, error)
ValidateClientRedirectURI(clientID, redirectURI string) bool
}
// DefaultSSOClientsStore 默认内存存储
type DefaultSSOClientsStore struct {
mu sync.RWMutex
clients map[string]*SSOClient
}
// NewDefaultSSOClientsStore 创建默认客户端存储
func NewDefaultSSOClientsStore() *DefaultSSOClientsStore {
return &DefaultSSOClientsStore{
clients: make(map[string]*SSOClient),
}
}
// RegisterClient 注册客户端
func (s *DefaultSSOClientsStore) RegisterClient(client *SSOClient) {
s.mu.Lock()
defer s.mu.Unlock()
s.clients[client.ClientID] = client
}
// GetByClientID 根据 ClientID 获取客户端
func (s *DefaultSSOClientsStore) GetByClientID(clientID string) (*SSOClient, error) {
s.mu.RLock()
defer s.mu.RUnlock()
client, ok := s.clients[clientID]
if !ok {
return nil, fmt.Errorf("client not found: %s", clientID)
}
return client, nil
}
// ValidateClientRedirectURI 验证客户端的 RedirectURI
func (s *DefaultSSOClientsStore) ValidateClientRedirectURI(clientID, redirectURI string) bool {
client, err := s.GetByClientID(clientID)
if err != nil {
return false
}
for _, uri := range client.RedirectURIs {
if uri == redirectURI {
return true
}
}
return false
}