Files
user-system/internal/api/handler/sso_handler.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

272 lines
7.1 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 handler
import (
"crypto/subtle"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/auth"
)
// SSOHandler SSO 处理程序
type SSOHandler struct {
ssoManager *auth.SSOManager
clientsStore auth.SSOClientsStore
}
// NewSSOHandler 创建 SSO 处理程序
func NewSSOHandler(ssoManager *auth.SSOManager, clientsStore auth.SSOClientsStore) *SSOHandler {
return &SSOHandler{
ssoManager: ssoManager,
clientsStore: clientsStore,
}
}
// AuthorizeRequest 授权请求
type AuthorizeRequest struct {
ClientID string `form:"client_id" binding:"required"`
RedirectURI string `form:"redirect_uri" binding:"required"`
ResponseType string `form:"response_type" binding:"required"`
Scope string `form:"scope"`
State string `form:"state"`
}
// Authorize 处理 SSO 授权请求
// GET /api/v1/sso/authorize?client_id=xxx&redirect_uri=xxx&response_type=code&scope=openid&state=xxx
func (h *SSOHandler) Authorize(c *gin.Context) {
var req AuthorizeRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 验证 response_type
if req.ResponseType != "code" && req.ResponseType != "token" {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported response_type"})
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
username, _ := c.Get("username")
// 生成授权码或 access token
if req.ResponseType == "code" {
code, err := h.ssoManager.GenerateAuthorizationCode(
req.ClientID,
req.RedirectURI,
req.Scope,
userID.(int64),
username.(string),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate code"})
return
}
// 重定向回客户端
redirectURL := req.RedirectURI + "?code=" + code
if req.State != "" {
redirectURL += "&state=" + req.State
}
c.Redirect(http.StatusFound, redirectURL)
} else {
// implicit 模式,直接返回 token
code, err := h.ssoManager.GenerateAuthorizationCode(
req.ClientID,
req.RedirectURI,
req.Scope,
userID.(int64),
username.(string),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate code"})
return
}
// 验证授权码获取 session
session, err := h.ssoManager.ValidateAuthorizationCode(code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate code"})
return
}
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"
if req.State != "" {
redirectURL += "&state=" + req.State
}
c.Redirect(http.StatusFound, redirectURL)
}
}
// TokenRequest Token 请求
type TokenRequest struct {
GrantType string `form:"grant_type" binding:"required"`
Code string `form:"code"`
RedirectURI string `form:"redirect_uri"`
ClientID string `form:"client_id" binding:"required"`
ClientSecret string `form:"client_secret" binding:"required"`
}
// TokenResponse Token 响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
Scope string `json:"scope"`
}
// Token 处理 Token 请求(授权码模式第二步)
// POST /api/v1/sso/token
func (h *SSOHandler) Token(c *gin.Context) {
var req TokenRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 验证 grant_type
if req.GrantType != "authorization_code" {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported grant_type"})
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid code"})
return
}
// 生成 access token
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,
TokenType: "Bearer",
ExpiresIn: int64(time.Until(expiresAt).Seconds()),
Scope: session.Scope,
})
}
// IntrospectRequest Introspect 请求
type IntrospectRequest struct {
Token string `form:"token" binding:"required"`
ClientID string `form:"client_id"`
}
// IntrospectResponse Introspect 响应
type IntrospectResponse struct {
Active bool `json:"active"`
UserID int64 `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Scope string `json:"scope,omitempty"`
}
// Introspect 验证 access token
// POST /api/v1/sso/introspect
func (h *SSOHandler) Introspect(c *gin.Context) {
var req IntrospectRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
info, err := h.ssoManager.IntrospectToken(req.Token)
if err != nil {
c.JSON(http.StatusOK, IntrospectResponse{Active: false})
return
}
c.JSON(http.StatusOK, IntrospectResponse{
Active: info.Active,
UserID: info.UserID,
Username: info.Username,
ExpiresAt: info.ExpiresAt.Unix(),
Scope: info.Scope,
})
}
// RevokeRequest 撤销请求
type RevokeRequest struct {
Token string `form:"token" binding:"required"`
}
// Revoke 撤销 access token
// POST /api/v1/sso/revoke
func (h *SSOHandler) Revoke(c *gin.Context) {
var req RevokeRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.ssoManager.RevokeToken(req.Token)
c.JSON(http.StatusOK, gin.H{"message": "token revoked"})
}
// UserInfoResponse 用户信息响应
type UserInfoResponse struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
}
// UserInfo 获取当前用户信息SSO 专用)
// GET /api/v1/sso/userinfo
func (h *SSOHandler) UserInfo(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
username, _ := c.Get("username")
c.JSON(http.StatusOK, UserInfoResponse{
UserID: userID.(int64),
Username: username.(string),
})
}