package lkgateway // Защита логина DEPO от блокировки (06.10.2026). На SQL Server логины с CHECK_POLICY подчиняются политике // блокировки Windows: несколько неудачных входов подряд — и логин заблокирован для всех, кто им пользуется. // Службы bj ходят в DEPO каждые несколько секунд; с неверным паролем (сменили на сервере, опечатка в // настройке, второй bj с тем же логином) они за минуты блокировали логин — так 06.10 тестовая ВМ с логином // bj_user остановила обмен боевого bj. Поэтому после неудачного входа bj с этими логином и паролем больше // не пробует depoLoginPause — кроме входа, который человек запустил сам (проверка в мастере, кнопки // настройки): для него depoLoginForce. Новый пароль — новая попытка сразу. import ( "context" "crypto/sha256" "database/sql" "database/sql/driver" "encoding/hex" "errors" "fmt" "strconv" "sync" "time" "github.com/jackc/pgx/v5/pgconn" mssql "github.com/microsoft/go-mssqldb" ) // depoLoginPause — сколько не повторять вход после неудачи (политика блокировки считает попытки за окно; // раз в 15 минут — заведомо реже порога). const depoLoginPause = 15 * time.Minute type depoLoginFail struct { at time.Time err string } var depoLoginFails = struct { sync.Mutex m map[string]depoLoginFail }{m: map[string]depoLoginFail{}} type depoLoginForceKey struct{} // depoLoginForce — вход по действию человека: выполняется и во время паузы после неудачи. func depoLoginForce(ctx context.Context) context.Context { return context.WithValue(ctx, depoLoginForceKey{}, true) } // depoLoginKey — сервер, логин и отпечаток пароля (сам пароль в памяти ключом не держим). func depoLoginKey(s FancyMSSQLSettings) string { h := sha256.Sum256([]byte(s.Password)) return fancyDriver(s) + "|" + s.Server + "|" + strconv.Itoa(s.Port) + "|" + s.User + "|" + hex.EncodeToString(h[:8]) } // depoIsLoginFailure — сервер отверг логин или пароль (то, что считает политика блокировки). func depoIsLoginFailure(err error) bool { var me mssql.Error if errors.As(err, &me) { switch me.Number { case 18456, 18486, 18487, 18488: // неверный логин/пароль, заблокирован, пароль истёк, нужно сменить return true } } var pe *pgconn.PgError if errors.As(err, &pe) { return pe.Code == "28P01" || pe.Code == "28000" } return false } // fancyConnector — подключение к DEPO через паузу после неудачного входа. type fancyConnector struct { base driver.Connector key string user string srv string } func (c *fancyConnector) Driver() driver.Driver { return c.base.Driver() } func (c *fancyConnector) Connect(ctx context.Context) (driver.Conn, error) { force, _ := ctx.Value(depoLoginForceKey{}).(bool) depoLoginFails.Lock() f, failed := depoLoginFails.m[c.key] depoLoginFails.Unlock() if failed && !force && time.Since(f.at) < depoLoginPause { return nil, fmt.Errorf("вход в DEPO логином %s не удался в %s (%s) — bj не повторяет его до %s, чтобы неудачные попытки не заблокировали логин на сервере. Проверьте логин и пароль: Настройка → «Учёт DEPO»", c.user, f.at.Format("15:04"), f.err, f.at.Add(depoLoginPause).Format("15:04")) } conn, err := c.base.Connect(ctx) switch { case err != nil && depoIsLoginFailure(err): depoLoginFails.Lock() depoLoginFails.m[c.key] = depoLoginFail{at: time.Now(), err: quikErrorDetail(err)} depoLoginFails.Unlock() if !failed && !force { raiseSystemAlert(globalRC, "DEPO: вход логином "+c.user+" не удался", "Сервер "+c.srv+" отверг логин или пароль bj. Чтобы неудачные попытки не заблокировали логин, bj не повторяет вход "+ strconv.Itoa(int(depoLoginPause/time.Minute))+" минут. Проверьте логин и пароль в настройке «Учёт DEPO».\n\n"+quikErrorDetail(err)) } case err == nil && failed: depoLoginFails.Lock() delete(depoLoginFails.m, c.key) depoLoginFails.Unlock() } return conn, err } // fancyOpenGated — пул подключений к DEPO с паузой после неудачного входа. func fancyOpenGated(s FancyMSSQLSettings, dsn string) (*sql.DB, error) { probe, err := sql.Open(fancyDriver(s), dsn) // без подключения: только чтобы взять драйвер if err != nil { return nil, err } drv := probe.Driver() _ = probe.Close() dc, ok := drv.(driver.DriverContext) if !ok { return sql.Open(fancyDriver(s), dsn) } base, err := dc.OpenConnector(dsn) if err != nil { return nil, err } return sql.OpenDB(&fancyConnector{base: base, key: depoLoginKey(s), user: s.User, srv: s.Server}), nil }