Files
user-system/internal/api/handler/sso_handler.go
long-agent 582ad7a069 test: add comprehensive test coverage and improve code quality
- Add new test files for auth, service, and handler modules
- Improve test organization and coverage
- Refactor code for better maintainability
- Add captcha, settings, stats, and theme handler tests
- Add auth module tests (CAS, OAuth, password, SSO, state)
- Add service layer tests for auth, export, permissions, roles
- All Go tests pass (exit code 0)
- All frontend tests pass (325 tests in 59 files)
2026-04-17 20:43:50 +08:00

332 lines
9.8 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 授权请求
// @Summary SSO 授权
// @Description 处理 SSO 授权请求,返回授权码或访问令牌
// @Tags SSO
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param client_id query string true "客户端ID"
// @Param redirect_uri query string true "回调地址"
// @Param response_type query string true "响应类型" Enums(code, token)
// @Param scope query string false "授权范围"
// @Param state query string false "状态参数"
// @Success 302 {string} string "重定向到回调地址"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "未认证"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/sso/authorize [get]
func (h *SSOHandler) Authorize(c *gin.Context) {
var req AuthorizeRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// 验证 response_type
if req.ResponseType != "code" && req.ResponseType != "token" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "unsupported response_type"})
return
}
// 验证 redirect_uri 是否在白名单中
if h.clientsStore != nil {
if !h.clientsStore.ValidateClientRedirectURI(req.ClientID, req.RedirectURI) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid redirect_uri"})
return
}
}
// 获取当前登录用户(从 auth middleware 设置的 context
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "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{"code": 500, "message": "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{"code": 500, "message": "failed to generate code"})
return
}
// 验证授权码获取 session
session, err := h.ssoManager.ValidateAuthorizationCode(code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to validate code"})
return
}
token, _, err := h.ssoManager.GenerateAccessToken(req.ClientID, session)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "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 请求
// @Summary 获取 Access Token
// @Description 使用授权码获取 Access Token授权码模式第二步
// @Tags SSO
// @Accept json
// @Produce json
// @Param grant_type formData string true "授权类型" Enums(authorization_code)
// @Param code formData string false "授权码"
// @Param redirect_uri formData string false "回调地址"
// @Param client_id formData string true "客户端ID"
// @Param client_secret formData string true "客户端密钥"
// @Success 200 {object} TokenResponse "访问令牌响应"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "客户端认证失败"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/sso/token [post]
func (h *SSOHandler) Token(c *gin.Context) {
var req TokenRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// 验证 grant_type
if req.GrantType != "authorization_code" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "unsupported grant_type"})
return
}
// 验证客户端凭证
if h.clientsStore != nil {
client, err := h.clientsStore.GetByClientID(req.ClientID)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client"})
return
}
// 使用常量时间比较防止时序攻击
if subtle.ConstantTimeCompare([]byte(req.ClientSecret), []byte(client.ClientSecret)) != 1 {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client_secret"})
return
}
}
// 验证授权码
session, err := h.ssoManager.ValidateAuthorizationCode(req.Code)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid code"})
return
}
// 生成 access token
token, expiresAt, err := h.ssoManager.GenerateAccessToken(req.ClientID, session)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "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
// @Summary 验证 Access Token
// @Description 验证 Access Token 的有效性并返回相关信息
// @Tags SSO
// @Accept json
// @Produce json
// @Param token formData string true "Access Token"
// @Param client_id formData string false "客户端ID"
// @Success 200 {object} IntrospectResponse "Token信息"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/sso/introspect [post]
func (h *SSOHandler) Introspect(c *gin.Context) {
var req IntrospectRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": 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
// @Summary 撤销 Access Token
// @Description 撤销指定的 Access Token
// @Tags SSO
// @Accept json
// @Produce json
// @Param token formData string true "Access Token"
// @Success 200 {object} Response "撤销成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/sso/revoke [post]
func (h *SSOHandler) Revoke(c *gin.Context) {
var req RevokeRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
h.ssoManager.RevokeToken(req.Token)
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "token revoked"})
}
// UserInfoResponse 用户信息响应
type UserInfoResponse struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
}
// UserInfo 获取当前用户信息
// @Summary 获取 SSO 用户信息
// @Description 获取当前通过 SSO 授权的用户信息
// @Tags SSO
// @Produce json
// @Security BearerAuth
// @Success 200 {object} Response{data=UserInfoResponse} "用户信息"
// @Failure 401 {object} Response "未认证"
// @Failure 500 {object} Response "服务器错误"
// @Router /api/v1/sso/userinfo [get]
func (h *SSOHandler) UserInfo(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
username, _ := c.Get("username")
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": UserInfoResponse{
UserID: userID.(int64),
Username: username.(string),
},
})
}