feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-03 17:38:31 +08:00
|
|
|
|
"crypto/subtle"
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
"net/http"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/user-management-system/internal/auth"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// SSOHandler SSO 处理程序
|
|
|
|
|
|
type SSOHandler struct {
|
2026-04-03 17:38:31 +08:00
|
|
|
|
ssoManager *auth.SSOManager
|
|
|
|
|
|
clientsStore auth.SSOClientsStore
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewSSOHandler 创建 SSO 处理程序
|
2026-04-03 17:38:31 +08:00
|
|
|
|
func NewSSOHandler(ssoManager *auth.SSOManager, clientsStore auth.SSOClientsStore) *SSOHandler {
|
|
|
|
|
|
return &SSOHandler{
|
|
|
|
|
|
ssoManager: ssoManager,
|
|
|
|
|
|
clientsStore: clientsStore,
|
|
|
|
|
|
}
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:38:31 +08:00
|
|
|
|
// 验证 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
// 获取当前登录用户(从 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:38:31 +08:00
|
|
|
|
token, _, err := h.ssoManager.GenerateAccessToken(req.ClientID, session)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 重定向回客户端,带 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:38:31 +08:00
|
|
|
|
// 验证客户端凭证
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
// 验证授权码
|
|
|
|
|
|
session, err := h.ssoManager.ValidateAuthorizationCode(req.Code)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid code"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成 access token
|
2026-04-03 17:38:31 +08:00
|
|
|
|
token, expiresAt, err := h.ssoManager.GenerateAccessToken(req.ClientID, session)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|