handler.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package auth
  2. import (
  3. "net/http"
  4. "github.com/gin-gonic/gin"
  5. )
  6. // 认证相关的 HTTP handler。
  7. //
  8. // 依据 docs/api.md 第二节。约定(architecture.md 5.2):
  9. // *gin.Context 只出现在这一层,业务逻辑在本包的 Service 里。
  10. // 注入 context 的键,供审计与 handler 使用。
  11. const (
  12. ctxKeySessionID = "session_id"
  13. ctxKeyTokenID = "token_id"
  14. ctxKeyClientIP = "client_ip"
  15. ctxKeyUserAgent = "user_agent"
  16. )
  17. // Handler 承载认证相关的 HTTP handler。
  18. type Handler struct {
  19. svc *Service
  20. store Store
  21. issuer string // HAVEN_TOTP_ISSUER
  22. // cookie 下发的 Domain / Secure,来自 HAVEN_COOKIE_DOMAIN 与 HAVEN_COOKIE_SECURE。
  23. CookieDomain string
  24. CookieSecure bool
  25. // 下面的钩子由 main 注入,避免本包依赖 audit(审计包反过来要用它的常量)。
  26. OnLoginSuccess func(c *gin.Context, e LoginAudit)
  27. OnLoginFailure func(c *gin.Context, username, reason string)
  28. OnLogout func(c *gin.Context, ac AuditContext)
  29. OnTotpBound func(c *gin.Context, ac AuditContext)
  30. OnTotpDisabled func(c *gin.Context, ac AuditContext)
  31. OnPasswordChanged func(c *gin.Context, ac AuditContext)
  32. OnTrustedDeviceAdd func(c *gin.Context, ac AuditContext, deviceName string)
  33. OnSessionRevoked func(c *gin.Context, ac AuditContext, sessionID, sessionCreatedAt string)
  34. OnDeviceRevoked func(c *gin.Context, ac AuditContext, deviceName string)
  35. OnTokenCreated func(c *gin.Context, ac AuditContext, name, scope, prefix string)
  36. OnTokenRevoked func(c *gin.Context, ac AuditContext, name, prefix string)
  37. }
  38. // NewHandler 构造 Handler。
  39. func NewHandler(svc *Service, store Store, issuer, cookieDomain string, cookieSecure bool) *Handler {
  40. return &Handler{
  41. svc: svc,
  42. store: store,
  43. issuer: issuer,
  44. CookieDomain: cookieDomain,
  45. CookieSecure: cookieSecure,
  46. }
  47. }
  48. // auditContext 是写审计事件所需的调用方信息。
  49. type AuditContext struct {
  50. UserID string
  51. Username string
  52. IP string
  53. UA string
  54. }
  55. // LoginAudit 是登录成功时的审计信息。
  56. type LoginAudit struct {
  57. UserID string
  58. Username string
  59. Via string
  60. }
  61. // 请求体(api.md 2.1 – 2.7)。
  62. type loginRequest struct {
  63. Username string `json:"username" binding:"required"`
  64. Password string `json:"password" binding:"required"`
  65. TrustDevice bool `json:"trust_device"`
  66. }
  67. type loginTotpRequest struct {
  68. TempToken string `json:"temp_token" binding:"required"`
  69. Code string `json:"code" binding:"required"`
  70. }
  71. type totpVerifyRequest struct {
  72. Secret string `json:"secret" binding:"required"`
  73. Code string `json:"code" binding:"required"`
  74. }
  75. type totpDisableRequest struct {
  76. Code string `json:"code" binding:"required"`
  77. }
  78. type passwordChangeRequest struct {
  79. OldPassword string `json:"old_password" binding:"required"`
  80. NewPassword string `json:"new_password" binding:"required"`
  81. TotpCode string `json:"totp_code"`
  82. }
  83. // 响应体。
  84. type userBriefResponse struct {
  85. ID string `json:"id"`
  86. Username string `json:"username"`
  87. }
  88. type loginResponse struct {
  89. User userBriefResponse `json:"user"`
  90. TotpRequired bool `json:"totp_required"`
  91. TempToken string `json:"temp_token,omitempty"`
  92. }
  93. type meResponse struct {
  94. Anonymous bool `json:"anonymous"`
  95. AuthType string `json:"auth_type,omitempty"`
  96. Scope string `json:"scope,omitempty"`
  97. User *userBriefResponse `json:"user,omitempty"`
  98. }
  99. func (h *Handler) auditContext(c *gin.Context) AuditContext {
  100. id := CurrentIdentity(c)
  101. ac := AuditContext{IP: c.ClientIP(), UA: c.Request.UserAgent()}
  102. if id.IsLoggedIn() {
  103. ac.UserID = id.UserID
  104. // 用户名只是审计快照,取不到时留空串(audit_events.actor_username 允许空)
  105. if u, err := h.svc.findUserByID(c.Request.Context(), id.UserID); err == nil {
  106. ac.Username = u.Username
  107. }
  108. }
  109. return ac
  110. }
  111. // setSessionCookie 下发会话 cookie。属性见 auth.md 第三节规则 2:
  112. // HttpOnly、SameSite=Lax、Domain=配置值;Secure 由 HAVEN_COOKIE_SECURE 决定。
  113. func setSessionCookie(c *gin.Context, domain string, secure bool, token string) {
  114. http.SetCookie(c.Writer, &http.Cookie{
  115. Name: CookieSession,
  116. Value: token,
  117. Path: "/",
  118. Domain: domain,
  119. MaxAge: int(SessionTTL.Seconds()),
  120. HttpOnly: true,
  121. Secure: secure,
  122. SameSite: http.SameSiteLaxMode,
  123. })
  124. }
  125. func setTrustedDeviceCookie(c *gin.Context, domain string, secure bool, token string) {
  126. http.SetCookie(c.Writer, &http.Cookie{
  127. Name: CookieTrustedDevice,
  128. Value: token,
  129. Path: "/",
  130. Domain: domain,
  131. MaxAge: int(TrustedDeviceTTL.Seconds()),
  132. HttpOnly: true,
  133. Secure: secure,
  134. SameSite: http.SameSiteLaxMode,
  135. })
  136. }
  137. func clearSessionCookie(c *gin.Context, domain string, secure bool) {
  138. http.SetCookie(c.Writer, &http.Cookie{
  139. Name: CookieSession,
  140. Value: "",
  141. Path: "/",
  142. Domain: domain,
  143. MaxAge: -1,
  144. HttpOnly: true,
  145. Secure: secure,
  146. SameSite: http.SameSiteLaxMode,
  147. })
  148. }
  149. // SessionID 返回当前请求所用会话的 id;Token 或匿名返回空串。
  150. func SessionID(c *gin.Context) string {
  151. if v, ok := c.Get(ctxKeySessionID); ok {
  152. if s, ok := v.(string); ok {
  153. return s
  154. }
  155. }
  156. return ""
  157. }
  158. // UserID 返回当前身份的用户 id。
  159. func UserID(c *gin.Context) string {
  160. return CurrentIdentity(c).UserID
  161. }
苏ICP备2026082406号
苏公网安备 待审核