package middleware import ( "errors" "strings" "github.com/gin-gonic/gin" "hy2xs-admin/model/constant" "hy2xs-admin/model/vo" "hy2xs-admin/service" ) // Отказ аутентификации несёт КОД состояния сессии. // // Раньше все ветки здесь звали vo.Fail с человеческой строкой, а код ответа // выводился в vo сравнением этой строки с тремя известными литералами. Под // условия подходил только `unauthorized`; `token expired` и `authentication // failed` уезжали к панели как обычная системная ошибка с кодом 50000. // // Следствие было видимым для оператора: истёкшая сессия на открытой странице // давала голый тост «token expired», ветка «войдите заново» не срабатывала // никогда, а перебросить на форму входа мог только переход по маршруту, // которому потребовался бы getAdminInfo. Ключ локализации `common.sessionExpired` // при этом существовал и был мёртвым. func JWTHandler() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.Request.Header.Get("Authorization") if authHeader == "" { vo.FailUnauthorized(constant.ErrCodeUnauthorized, constant.UnauthorizedError, c) c.Abort() return } parts := strings.SplitN(authHeader, " ", 2) if !(len(parts) == 2 && parts[0] == "Bearer") { vo.FailUnauthorized(constant.ErrCodeTokenInvalid, constant.IllegalTokenError, c) c.Abort() return } myClaims, err := service.ParseToken(parts[1]) if err != nil { vo.FailUnauthorized(tokenErrorCode(err), err.Error(), c) c.Abort() return } admin, err := service.GetAdminForTokenValidation(myClaims.Admin.Id) if err != nil { // Это уже не состояние сессии, а отказ чтения учётной записи: // сворачивать его в «войдите заново» значило бы отправлять // оператора на форму входа при недоступной базе. vo.Fail(err.Error(), c) c.Abort() return } if admin.Status != nil && *admin.Status != 1 { vo.FailUnauthorized( constant.ErrCodeAccountDisabled, "this account has been disabled", c, ) c.Abort() return } tokenVersion := int64(1) if admin.TokenVersion != nil && *admin.TokenVersion > 0 { tokenVersion = *admin.TokenVersion } if myClaims.Admin.TokenVersion != tokenVersion { // Версия токена сменилась: пароль изменён или доступ отозван. // Для оператора это неотличимо от истёкшей сессии — вход заново. vo.FailUnauthorized(constant.ErrCodeSessionExpired, constant.IllegalTokenError, c) c.Abort() return } c.Set("adminClaims", myClaims.Admin) c.Next() } } // tokenErrorCode различает истёкший токен и недействительный. // // Вопрос задаётся ЗНАЧЕНИЮ ошибки, а не её тексту: service.ParseToken // возвращает объявленные значения, поэтому правка формулировки сообщения не // может молча превратить истёкшую сессию в неизвестную ошибку. // // Отказ прочитать ключ подписи (недоступная база) сюда тоже приходит, и это // НЕ состояние сессии. Отдельного кода он не получает намеренно: снаружи // панели такой отказ неотличим от недействительного токена, и предлагать // оператору войти заново — единственное осмысленное действие, которое ему // доступно. func tokenErrorCode(err error) string { if errors.Is(err, service.ErrTokenExpired) { return constant.ErrCodeSessionExpired } return constant.ErrCodeTokenInvalid }