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 - 响应格式协议中间件 - 导出无界查询修复
This commit is contained in:
@@ -6,9 +6,17 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxSessions 最大 session 数量限制
|
||||
MaxSessions = 10000
|
||||
// CleanupInterval 清理间隔
|
||||
CleanupInterval = 5 * time.Minute
|
||||
)
|
||||
|
||||
// SSOOAuth2Config SSO OAuth2 配置
|
||||
type SSOOAuth2Config struct {
|
||||
ClientID string
|
||||
@@ -66,6 +74,7 @@ type SSOSession struct {
|
||||
|
||||
// SSOManager SSO 管理器
|
||||
type SSOManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*SSOSession
|
||||
}
|
||||
|
||||
@@ -76,12 +85,35 @@ func NewSSOManager() *SSOManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := generateSecureToken(32)
|
||||
code, err := generateSecureToken(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sessionID, err := generateSecureToken(16)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
session := &SSOSession{
|
||||
SessionID: generateSecureToken(16),
|
||||
SessionID: sessionID,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
ClientID: clientID,
|
||||
@@ -90,13 +122,26 @@ func (m *SSOManager) GenerateAuthorizationCode(clientID, redirectURI, scope stri
|
||||
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")
|
||||
@@ -114,8 +159,11 @@ func (m *SSOManager) ValidateAuthorizationCode(code string) (*SSOSession, error)
|
||||
}
|
||||
|
||||
// GenerateAccessToken 生成访问令牌
|
||||
func (m *SSOManager) GenerateAccessToken(clientID string, session *SSOSession) (string, time.Time) {
|
||||
token := generateSecureToken(32)
|
||||
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{
|
||||
@@ -128,22 +176,37 @@ func (m *SSOManager) GenerateAccessToken(clientID string, session *SSOSession) (
|
||||
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
|
||||
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,
|
||||
@@ -157,12 +220,21 @@ func (m *SSOManager) IntrospectToken(token string) (*SSOTokenInfo, error) {
|
||||
|
||||
// RevokeToken 撤销 token
|
||||
func (m *SSOManager) RevokeToken(token string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.sessions, token)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpired 清理过期的 session(可由后台 goroutine 定期调用)
|
||||
// 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) {
|
||||
@@ -171,11 +243,38 @@ func (m *SSOManager) CleanupExpired() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func generateSecureToken(length int) (string, error) {
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return base64.URLEncoding.EncodeToString(bytes)[: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 客户端配置存储
|
||||
@@ -189,10 +288,12 @@ type SSOClient struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -205,11 +306,15 @@ func NewDefaultSSOClientsStore() *DefaultSSOClientsStore {
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user