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:
2026-04-03 17:38:31 +08:00
parent 44e60be918
commit 765a50b7d4
22 changed files with 2318 additions and 71 deletions

View File

@@ -1,6 +1,7 @@
package handler
import (
"crypto/subtle"
"net/http"
"time"
@@ -11,12 +12,16 @@ import (
// SSOHandler SSO 处理程序
type SSOHandler struct {
ssoManager *auth.SSOManager
ssoManager *auth.SSOManager
clientsStore auth.SSOClientsStore
}
// NewSSOHandler 创建 SSO 处理程序
func NewSSOHandler(ssoManager *auth.SSOManager) *SSOHandler {
return &SSOHandler{ssoManager: ssoManager}
func NewSSOHandler(ssoManager *auth.SSOManager, clientsStore auth.SSOClientsStore) *SSOHandler {
return &SSOHandler{
ssoManager: ssoManager,
clientsStore: clientsStore,
}
}
// AuthorizeRequest 授权请求
@@ -43,6 +48,14 @@ func (h *SSOHandler) Authorize(c *gin.Context) {
return
}
// 验证 redirect_uri 是否在白名单中
if h.clientsStore != nil {
if !h.clientsStore.ValidateClientRedirectURI(req.ClientID, req.RedirectURI) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid redirect_uri"})
return
}
}
// 获取当前登录用户(从 auth middleware 设置的 context
userID, exists := c.Get("user_id")
if !exists {
@@ -93,7 +106,11 @@ func (h *SSOHandler) Authorize(c *gin.Context) {
return
}
token, _ := h.ssoManager.GenerateAccessToken(req.ClientID, session)
token, _, err := h.ssoManager.GenerateAccessToken(req.ClientID, session)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
// 重定向回客户端,带 token
redirectURL := req.RedirectURI + "#access_token=" + token + "&expires_in=7200"
@@ -136,6 +153,20 @@ func (h *SSOHandler) Token(c *gin.Context) {
return
}
// 验证客户端凭证
if h.clientsStore != nil {
client, err := h.clientsStore.GetByClientID(req.ClientID)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid client"})
return
}
// 使用常量时间比较防止时序攻击
if subtle.ConstantTimeCompare([]byte(req.ClientSecret), []byte(client.ClientSecret)) != 1 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid client_secret"})
return
}
}
// 验证授权码
session, err := h.ssoManager.ValidateAuthorizationCode(req.Code)
if err != nil {
@@ -144,7 +175,11 @@ func (h *SSOHandler) Token(c *gin.Context) {
}
// 生成 access token
token, expiresAt := h.ssoManager.GenerateAccessToken(req.ClientID, session)
token, expiresAt, err := h.ssoManager.GenerateAccessToken(req.ClientID, session)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
c.JSON(http.StatusOK, TokenResponse{
AccessToken: token,

View File

@@ -34,6 +34,7 @@ func NewAuthMiddleware(
roleRepo *repository.RoleRepository,
rolePermissionRepo *repository.RolePermissionRepository,
permissionRepo *repository.PermissionRepository,
l1Cache *cache.L1Cache,
) *AuthMiddleware {
return &AuthMiddleware{
jwt: jwt,
@@ -42,7 +43,7 @@ func NewAuthMiddleware(
roleRepo: roleRepo,
rolePermissionRepo: rolePermissionRepo,
permissionRepo: permissionRepo,
l1Cache: cache.NewL1Cache(),
l1Cache: l1Cache,
}
}
@@ -129,7 +130,7 @@ func (m *AuthMiddleware) isJTIBlacklisted(jti string) bool {
}
func (m *AuthMiddleware) loadUserRolesAndPerms(ctx context.Context, userID int64) ([]string, []string) {
if m.userRoleRepo == nil || m.roleRepo == nil || m.rolePermissionRepo == nil || m.permissionRepo == nil {
if m.userRoleRepo == nil {
return nil, nil
}
@@ -140,34 +141,9 @@ func (m *AuthMiddleware) loadUserRolesAndPerms(ctx context.Context, userID int64
}
}
roleIDs, err := m.userRoleRepo.GetRoleIDsByUserID(ctx, userID)
if err != nil || len(roleIDs) == 0 {
return nil, nil
}
// 收集所有角色ID包括直接分配的角色和所有祖先角色
allRoleIDs := make([]int64, 0, len(roleIDs)*2)
allRoleIDs = append(allRoleIDs, roleIDs...)
for _, roleID := range roleIDs {
ancestorIDs, err := m.roleRepo.GetAncestorIDs(ctx, roleID)
if err == nil && len(ancestorIDs) > 0 {
allRoleIDs = append(allRoleIDs, ancestorIDs...)
}
}
// 去重
seen := make(map[int64]bool)
uniqueRoleIDs := make([]int64, 0, len(allRoleIDs))
for _, id := range allRoleIDs {
if !seen[id] {
seen[id] = true
uniqueRoleIDs = append(uniqueRoleIDs, id)
}
}
roles, err := m.roleRepo.GetByIDs(ctx, roleIDs)
if err != nil {
// 使用已优化的单次 JOIN 查询获取用户角色和权限
roles, permissions, err := m.userRoleRepo.GetUserRolesAndPermissions(ctx, userID)
if err != nil || len(roles) == 0 {
return nil, nil
}
@@ -176,24 +152,12 @@ func (m *AuthMiddleware) loadUserRolesAndPerms(ctx context.Context, userID int64
roleCodes = append(roleCodes, role.Code)
}
permissionIDs, err := m.rolePermissionRepo.GetPermissionIDsByRoleIDs(ctx, uniqueRoleIDs)
if err != nil || len(permissionIDs) == 0 {
entry := userPermEntry{roles: roleCodes, perms: []string{}}
m.l1Cache.Set(cacheKey, entry, 30*time.Minute) // PERF-01 优化:增加缓存 TTL 减少 DB 查询
return entry.roles, entry.perms
}
permissions, err := m.permissionRepo.GetByIDs(ctx, permissionIDs)
if err != nil {
return roleCodes, nil
}
permCodes := make([]string, 0, len(permissions))
for _, permission := range permissions {
permCodes = append(permCodes, permission.Code)
for _, perm := range permissions {
permCodes = append(permCodes, perm.Code)
}
m.l1Cache.Set(cacheKey, userPermEntry{roles: roleCodes, perms: permCodes}, 30*time.Minute) // PERF-01 优化:增加缓存 TTL 减少 DB 查询
m.l1Cache.Set(cacheKey, userPermEntry{roles: roleCodes, perms: permCodes}, 30*time.Minute)
return roleCodes, permCodes
}