| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- package auth
- import (
- "net/http"
- "github.com/gin-gonic/gin"
- )
- // 认证相关的 HTTP handler。
- //
- // 依据 docs/api.md 第二节。约定(architecture.md 5.2):
- // *gin.Context 只出现在这一层,业务逻辑在本包的 Service 里。
- // 注入 context 的键,供审计与 handler 使用。
- const (
- ctxKeySessionID = "session_id"
- ctxKeyTokenID = "token_id"
- ctxKeyClientIP = "client_ip"
- ctxKeyUserAgent = "user_agent"
- )
- // Handler 承载认证相关的 HTTP handler。
- type Handler struct {
- svc *Service
- store Store
- issuer string // HAVEN_TOTP_ISSUER
- // cookie 下发的 Domain / Secure,来自 HAVEN_COOKIE_DOMAIN 与 HAVEN_COOKIE_SECURE。
- CookieDomain string
- CookieSecure bool
- // 下面的钩子由 main 注入,避免本包依赖 audit(审计包反过来要用它的常量)。
- OnLoginSuccess func(c *gin.Context, e LoginAudit)
- OnLoginFailure func(c *gin.Context, username, reason string)
- OnLogout func(c *gin.Context, ac AuditContext)
- OnTotpBound func(c *gin.Context, ac AuditContext)
- OnTotpDisabled func(c *gin.Context, ac AuditContext)
- OnPasswordChanged func(c *gin.Context, ac AuditContext)
- OnTrustedDeviceAdd func(c *gin.Context, ac AuditContext, deviceName string)
- OnSessionRevoked func(c *gin.Context, ac AuditContext, sessionID, sessionCreatedAt string)
- OnDeviceRevoked func(c *gin.Context, ac AuditContext, deviceName string)
- OnTokenCreated func(c *gin.Context, ac AuditContext, name, scope, prefix string)
- OnTokenRevoked func(c *gin.Context, ac AuditContext, name, prefix string)
- }
- // NewHandler 构造 Handler。
- func NewHandler(svc *Service, store Store, issuer, cookieDomain string, cookieSecure bool) *Handler {
- return &Handler{
- svc: svc,
- store: store,
- issuer: issuer,
- CookieDomain: cookieDomain,
- CookieSecure: cookieSecure,
- }
- }
- // auditContext 是写审计事件所需的调用方信息。
- type AuditContext struct {
- UserID string
- Username string
- IP string
- UA string
- }
- // LoginAudit 是登录成功时的审计信息。
- type LoginAudit struct {
- UserID string
- Username string
- Via string
- }
- // 请求体(api.md 2.1 – 2.7)。
- type loginRequest struct {
- Username string `json:"username" binding:"required"`
- Password string `json:"password" binding:"required"`
- TrustDevice bool `json:"trust_device"`
- }
- type loginTotpRequest struct {
- TempToken string `json:"temp_token" binding:"required"`
- Code string `json:"code" binding:"required"`
- }
- type totpVerifyRequest struct {
- Secret string `json:"secret" binding:"required"`
- Code string `json:"code" binding:"required"`
- }
- type totpDisableRequest struct {
- Code string `json:"code" binding:"required"`
- }
- type passwordChangeRequest struct {
- OldPassword string `json:"old_password" binding:"required"`
- NewPassword string `json:"new_password" binding:"required"`
- TotpCode string `json:"totp_code"`
- }
- // 响应体。
- type userBriefResponse struct {
- ID string `json:"id"`
- Username string `json:"username"`
- }
- type loginResponse struct {
- User userBriefResponse `json:"user"`
- TotpRequired bool `json:"totp_required"`
- TempToken string `json:"temp_token,omitempty"`
- }
- type meResponse struct {
- Anonymous bool `json:"anonymous"`
- AuthType string `json:"auth_type,omitempty"`
- Scope string `json:"scope,omitempty"`
- User *userBriefResponse `json:"user,omitempty"`
- }
- func (h *Handler) auditContext(c *gin.Context) AuditContext {
- id := CurrentIdentity(c)
- ac := AuditContext{IP: c.ClientIP(), UA: c.Request.UserAgent()}
- if id.IsLoggedIn() {
- ac.UserID = id.UserID
- // 用户名只是审计快照,取不到时留空串(audit_events.actor_username 允许空)
- if u, err := h.svc.findUserByID(c.Request.Context(), id.UserID); err == nil {
- ac.Username = u.Username
- }
- }
- return ac
- }
- // setSessionCookie 下发会话 cookie。属性见 auth.md 第三节规则 2:
- // HttpOnly、SameSite=Lax、Domain=配置值;Secure 由 HAVEN_COOKIE_SECURE 决定。
- func setSessionCookie(c *gin.Context, domain string, secure bool, token string) {
- http.SetCookie(c.Writer, &http.Cookie{
- Name: CookieSession,
- Value: token,
- Path: "/",
- Domain: domain,
- MaxAge: int(SessionTTL.Seconds()),
- HttpOnly: true,
- Secure: secure,
- SameSite: http.SameSiteLaxMode,
- })
- }
- func setTrustedDeviceCookie(c *gin.Context, domain string, secure bool, token string) {
- http.SetCookie(c.Writer, &http.Cookie{
- Name: CookieTrustedDevice,
- Value: token,
- Path: "/",
- Domain: domain,
- MaxAge: int(TrustedDeviceTTL.Seconds()),
- HttpOnly: true,
- Secure: secure,
- SameSite: http.SameSiteLaxMode,
- })
- }
- func clearSessionCookie(c *gin.Context, domain string, secure bool) {
- http.SetCookie(c.Writer, &http.Cookie{
- Name: CookieSession,
- Value: "",
- Path: "/",
- Domain: domain,
- MaxAge: -1,
- HttpOnly: true,
- Secure: secure,
- SameSite: http.SameSiteLaxMode,
- })
- }
- // SessionID 返回当前请求所用会话的 id;Token 或匿名返回空串。
- func SessionID(c *gin.Context) string {
- if v, ok := c.Get(ctxKeySessionID); ok {
- if s, ok := v.(string); ok {
- return s
- }
- }
- return ""
- }
- // UserID 返回当前身份的用户 id。
- func UserID(c *gin.Context) string {
- return CurrentIdentity(c).UserID
- }
|