Mongoose: Sie müssen überprüfen, ob das Passwortfeld in pre findOneAndUpdate geändert wurde

Erstellt am 28. Sept. 2016  ·  24Kommentare  ·  Quelle: Automattic/mongoose

Ich bin gestern Abend auf ein Problem gestoßen und konnte immer noch keine Lösung finden, also kann mir hoffentlich jemand einen Rat geben. Ich erlaube Benutzern, ihren Benutzernamen und ihr Passwort zu aktualisieren, und jedes Mal, wenn das Passwortfeld geändert wurde, muss ich es hashen, bevor ich es in mongo speichere.

Bisher hatte ich diesen Code verwendet, um das Passwort des Benutzers zu hashen:

UserSchema.pre('save', function(next) {
    if (this.isModified('password')) // If the pw has been modified, then encrypt it again
    this.password = this.encryptPassword(this.password);

    next();
});

Die Funktion encryptPassword ist eine benutzerdefinierte Funktion, die ich zu meinem User Schema hinzugefügt habe:

// Add custom methods to our User schema
UserSchema.methods = {
    // Hash the password
    encryptPassword: function(plainTextPassword) {
        if (!plainTextPassword) {
            return ''
        } else {
            var salt = bcrypt.genSaltSync(10);
            return bcrypt.hashSync(plainTextPassword, salt);
        }
    }
};

Dies funktioniert jedoch nur, wenn sich der Benutzer zum ersten Mal anmeldet und ich eine neue Benutzerinstanz erstelle und dann speichere. Ich benötige die gleiche Funktionalität auch beim Aktualisieren des Benutzers.

Ich habe hier in früheren Problemen recherchiert und Pre-, Post-Middleware wird nicht auf findByIdAndUpdate ausgeführt und bin auf einen Kommentar Entwickler versucht hat, etwas sehr Ähnliches zu tun, was ich https:// github.com/Automattic/mongoose/issues/964#issuecomment -154332797

@vkarpov15 antwortete auf seine Frage mit dem folgenden

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { password: hashPassword(this.getUpdate().$set.password) });
});

Das einzige Problem ist, dass wir dadurch jedes Mal, wenn Sie einen Benutzer aktualisieren, das Passwort neu hacken, was falsch ist. Sie sollten das Passwort nur bei der ersten Anmeldung des Benutzers und immer dann, wenn er eine Anfrage zum Ändern seines Passworts sendet, mit einem Hashwert versehen. Wenn Sie den obigen Code jedes Mal ausführen, wenn der Benutzer aktualisiert wird, müssen Sie am Ende nur den alten Hash neu hashen. Wenn der Benutzer sein Passwort das nächste Mal auf dem Client eingibt, stimmt er nicht mehr mit dem in mongo gespeicherten Hash überein. .

Gibt es eine Möglichkeit, etwas wie this.isModified('password') innerhalb von .pre('findOneAndUpdate' , damit ich das Passwort nur dann hash, wenn der Benutzer es aktualisiert hat?

An dieser Stelle bin ich für alle Vorschläge offen danke.

Hilfreichster Kommentar

Ich verwende derzeit diese Lösung und es funktioniert. Sie müssen keine weitere Abfrage ausführen.

schema.pre("update", function(next) {
            const password = this.getUpdate().$set.password;
            if (!password) {
                return next();
            }
            try {
                const salt = Bcrypt.genSaltSync();
                const hash = Bcrypt.hashSync(password, salt);
                this.getUpdate().$set.password = hash;
                next();
            } catch (error) {
                return next(error);
            }
        });

Alle 24 Kommentare

Ich habe das noch etwas getestet und dachte, ich würde noch ein paar Notizen hinzufügen. An dieser Stelle kann ich user.save oder User.update .

Wenn ich versuche, den Benutzer mit user.save zu aktualisieren, tritt ein Validierungsfehler aufgrund eines benutzerdefinierten Validierungs-Hooks auf, den ich eingerichtet habe:

//// Custom error for unique username
UserSchema.path('username').validate(function(value, done) {
    mongoose.model('User', UserSchema).count({ username: value }, function(err, count) {
        if (err) {
            return done(err);
        } 
        // If `count` is greater than zero, "invalidate"
        done(!count);
    });
}, 'Sorry but this username is already taken');

Das Obige ist notwendig, damit ich eine benutzerdefinierte Validierungsfehlermeldung an den Client zurückgeben kann, wenn ein neuer Benutzer versucht, sich anzumelden und einen Benutzernamen wählt, mit dem sich bereits jemand anderes angemeldet hat. Wenn ich versuche, die Informationen eines Benutzers zu aktualisieren, indem ich user.save ausführe, wird dieser benutzerdefinierte Validator ausgeführt und gibt einen Fehler aus, da der Benutzername bereits vergeben ist.

Wenn ich stattdessen versuche, User.update verwenden... kann ich this.isModified('password') nicht aufrufen, da sich dies auf das Modell und nicht auf ein Dokument bezieht.

Ich stecke mal wieder fest und weiß nicht was ich tun soll. Hoffentlich habe ich genug Informationen geliefert. Dies ist das erste Problem, auf das ich bei Mungo gestoßen bin und das ich nicht alleine lösen konnte, daher ist jede Hilfe sehr dankbar, danke.

Ok, also habe ich in meinem ursprünglichen Kommentar erwähnt, dass @vkarpov15 eine mögliche Lösung in einem anderen Problem gepostet hat:

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { password: hashPassword(this.getUpdate().$set.password) });
});

Ich habe versucht, dies in einem Pre-Hook für findOneAndUpdate und update zu implementieren:

UserSchema.pre('findOneAndUpdate', function(next) {
    this.findOneAndUpdate({}, { password: encryptPassword(this.getUpdate().$set.password) });
    next();
});

Dies funktioniert und ich kann das gehashte Passwort protokollieren, aber jetzt kann ich es nicht über meine Passwortfeldvalidierung hinaus bekommen. Ich habe eine Passwortfeldvalidierung, die besagt, dass das Passwort nur 4 Zeichen lang sein darf. Ich bin nicht hier, um darüber zu diskutieren, wie kurz oder lang ein Passwort sein sollte ... der Punkt ist, dass der obige Hook nicht funktioniert, weil er nicht über die Validierung hinauskommt, während ich das Hashing durchgeführt habe im Hook vor save lief er, nachdem die Validierung stattgefunden hatte.

Immer noch nicht sicher, was zu diesem Zeitpunkt zu tun ist.

Also habe ich endlich eine Lösung dafür gefunden. Ich habe die Update-Methoden für diesen speziellen Anwendungsfall nicht mehr verwendet und wieder save . Ich habe den UserSchema.path('username') Code gelöscht, den ich zuvor geteilt habe, und so sieht mein Pre-Save-Hook jetzt aus:

UserSchema.pre('save', function(next, done) {
    var self = this;
    mongoose.models["User"].findOne({username: self.username}, function(err, user) {
        if(err) {
            done(err);
        } else if(user) {            
            if (user._id.equals(self._id)) return next(); // If id's are equal, then we don't care about false dupe username error because its the same user!

            self.invalidate('username', 'Sorry but this username is already taken');
            done(new Error('Sorry but this username is already taken'));
        } else {
            next();
        }
    });
});

Im obigen Code überprüfe ich, ob wir bereits einen Benutzer in mongo haben, dessen Benutzername versucht zu speichern. Wenn der Benutzername bereits existiert, vergleiche ich die _id's des gefundenen Benutzerdokuments und des Benutzerdokuments, das gespeichert werden soll. Wenn ihre _id's gleich sind, ist uns der Validierungsfehler des eindeutigen Benutzernamens egal, da es sich um denselben Benutzer handelt, also rufen wir einfach next() und fahren fort.

Dadurch kann auch mein Pre-Save-Hash für das Passwort-Hashing ausgeführt werden:

UserSchema.pre('save', function(next) {
    console.log('pre save password: ' + this.password);
    if (this.isModified('password')) // If the pw has been modified, then encrypt it again
        this.password = this.encryptPassword(this.password);
    next();
});

Ich kann also das Passwort immer noch hashen, wenn es aktualisiert wird, und ich bekomme immer noch alle meine Validierungsregeln ausgeführt, die ich für mein UserSchema . Ich habe dies in einem Demo-Projekt getestet und kann keine Fehler darin finden.

Ich werde dieses Problem schließen, aber nach der Integration in meine Produktions-App wiederkommen und ein Update mit meinem Fortschritt hinterlassen. Es fällt mir schwer zu glauben, dass ich der einzige bin, der jemals dieses Problem hatte, also würde ich immer noch gerne Ratschläge oder Feedback zu meiner Lösung hören oder wie Sie es selbst machen würden.

Vielen Dank.

Also habe ich die Lösung in meinem vorherigen Kommentar in einer Produktions-App getestet und festgestellt, dass sie fehlerhaft ist und nicht funktionieren wird. Wenn ich save im Benutzerdokument aufrufe und das Passwortfeld nicht aktualisiert wurde, ist es immer noch nur ein Hash ... es wird meine Überprüfung für mein Passwortfeld leider nicht bestehen.

Es sieht also so aus, als ob ich save ohne mehrere Problemumgehungen verwenden kann, was mich nur dazu bringt, mich wieder mit den update Funktionen und ihren oben bereits erwähnten Problemen zu befassen.

Wenn jemand keine Ahnung hat, wie ich das beheben kann, würde ich mich freuen, von Ihnen zu hören.

Es stellt sich heraus, dass dies nicht möglich ist. Es ist nur ein seltsamer Randfall, auf den Sie bei der Verwendung der Validierung stoßen. Am Ende habe ich die Mungo-Validierungen für mein Passwortfeld entfernt und es einfach manuell mit einem benutzerdefinierten Regex bearbeitet. Dies gab mir die volle Kontrolle und ermöglichte mir, alle Speicher-/Aktualisierungsszenarien für mein User Modell zu berücksichtigen.

Nun, es gibt eine Lösung, Sie verwenden update oder findOneAndUpdate und übergeben das Passwortfeld vom Client nur, wenn Sie meinen, dass das Passwort aktualisiert werden soll. IMO ist es sowieso keine gute Praxis, den Passwort-Hash im Benutzerdokument zu speichern, da Sie dann besonders diszipliniert sein müssen, damit Ihre API den Passwort-Hash nicht verliert

IMO ist es sowieso keine gute Praxis, den Passwort-Hash im Benutzerdokument zu speichern, da Sie dann besonders diszipliniert sein müssen, damit Ihre API den Passwort-Hash nicht verliert

Interessant. Wie würde eine Alternative dazu aussehen @vkarpov15?

Getrennte Sammlung mit 2 Feldern, ID und Passwort-Hash. Um das Passwort des Benutzers zu überprüfen, fragen Sie die Hashes-Sammlung nach ihrem Hash ab und überprüfen Sie ihn normal. Macht es einfacher, Passwortänderungen in ein ruhiges Paradigma einzupassen, und einfacher sicherzustellen, dass Sie das Benutzerpasswort nicht verlieren, wenn eine Liste von Benutzern angezeigt wird, da das Abrufen des Passwort-Hashs des Benutzers opt-in ist

@mitchellporter hallo...

Bin auf deinen Beitrag gestoßen, als ich mich mit dem gleichen Problem beschäftigte. Ich habe es gerade ausprobiert und denke, es wird meine Bedürfnisse decken. Wollte es teilen.

const bcrypt = require('bcrypt')
const debug = require('debug')('db')
const mongoose = require('mongoose')
mongoose.Promise = global.Promise;
const { Schema } = mongoose
const { Types } = Schema
const { ObjectId } = Types
const db = mongoose.connection

const UserSchema = new Schema({
  email: { type: String, index: { unique: true, dropDups: true } },
  hash: { type: String, required: true }
})

// This is the important bit
// Using a virtual lets me pass `{ password: 'xyz' }` 
// without actually having it save.
// Instead it is caught by this setter 
// which performs the hashing and 
// saves the hash to the document's hash property.
UserSchema.virtual('password').set(function(value) {
  const salt = bcrypt.genSaltSync(10)
  this.hash = bcrypt.hashSync(value, salt)
})

// A method for checking the password
UserSchema.methods.comparePassword = function(password) {
  return bcrypt.compareSync(password, this.hash)
}

// Here I make sure I never return the
// password in the JSON representation
// Note that I don't do the same to
// `toObject` so i can still see the hash
// in, say, the console.
UserSchema.set('toJSON', {
  getters: true,
  transform: (doc, ret, options) => {
    delete ret.hash;
    return ret;
  }
})

const User = mongoose.model('Store', UserSchema)

module.exports = { db }

So zeigt sich das bei der Ausgabe des Objekts an die Konsole:

{
  _id: '595317e5f3afec2e0453bc64',
  email: '[email protected]',
  hash: '$2a$10$Bovjefiwfdwu/q9liKpdyluQTFIFT/VcrpzUWYPc.bGUYUIFIG',
  __v: 2,
}

Und so sieht es aus, wenn es als JSON zurückgegeben wird:

{
  email: '[email protected]',
}

Dies trennt die Hashes nicht in eine separate Sammlung, wie @vkarpov15 vorgeschlagen hat, was ich früher als später tun möchte, aber vielleicht hilft es Ihrem Fall.

Nun... Das war Zeitverschwendung... Entschuldigung.

Der Komponententest, den ich verwendet habe, um dies zu überprüfen, war falsch. Es stellt sich heraus, dass nur die virtuellen Sätze im Speicher hashen und nicht beibehalten werden.

Also zurück zum Herausfinden, wie man Updates hashen kann.

@lgomez Ich habe mich seitdem von der Verwendung von MongoDB in meinen Projekten entfernt, also verwende ich auch nicht mehr mongoose . Viel Glück dabei!

Vielleicht könnte das helfen?

schema.pre('update', function(next) {
  this.findOne({"_id":this.getUpdate().$set._id},function(err, doc){
    if(doc.password != this.getUpdate().$set.password){
      this.getUpdate().$set.password = bcrypt.hashSync(this.getUpdate().$set.password, 10);
    }
    next();
  })
});

@NicolasBlois das funktioniert. Es gibt eine potenzielle Racebedingung, die jedoch in den meisten Fällen funktioniert.

Ich verwende derzeit diese Lösung und es funktioniert. Sie müssen keine weitere Abfrage ausführen.

schema.pre("update", function(next) {
            const password = this.getUpdate().$set.password;
            if (!password) {
                return next();
            }
            try {
                const salt = Bcrypt.genSaltSync();
                const hash = Bcrypt.hashSync(password, salt);
                this.getUpdate().$set.password = hash;
                next();
            } catch (error) {
                return next(error);
            }
        });

Was ist der Unterschied zwischen der obigen Lösung und so etwas, um die ersten vier Zeilen der Middleware zu ersetzen?
if (!this.isModified('password')) return next()

isModified wäre besser, wenn Ihr Passwort ein komplexes Objekt wäre, weil es alle seine Felder in der Tiefe durchläuft, aber es würde wahrscheinlich etwas mehr kosten. Obwohl ich davon ausgehen würde, dass es in diesem Anwendungsfall keinen großen Unterschied machen würde.

Durchläuft es alle Felder, wenn ein Pfad angegeben ist?

Keine Ahnung. Vielleicht schauen Sie in den Quellcode, um es herauszufinden?

Habe ich gemacht, aber danke für den Vorschlag.

@ezamelczyk danke für deine Antwort. Es funktioniert super!

Ich werde hier teilen, wie ich die Aktualisierungsanfrage für das Passwort stelle; falls die Leute danach suchen.

exports.updateUserPassword = ({_id}, passObj) => {
    const userId = _id;

    if(passObj.newPassword !== passObj.passwordConfirmation){
        throw new Error('New password and confirmation do not match!');
    }

    return new Promise(async(resolve, reject) => {
        try{
            await User.update({ _id: userId }, {
                $set: { password: passObj.newPassword }
            })            
            resolve({})
        }catch(error){
            reject({ message: error.message, error: error })
        }
    })
}

Obwohl dieses Update von der Einstellungsseite ausgelöst wird, nicht von "Passwort vergessen" und auf der Einstellungsseite muss der Benutzer das aktuelle Passwort eingeben, um das Passwort aktualisieren zu können. Ich muss also überprüfen, ob das aktuelle Passwort übereinstimmt. Ich habe die Methode ComparePassword zur Verfügung, kann aber nicht herausfinden, wie ich anrufe? passObj enthält currentPassword, newPassword, passwordConfirmation.

userSchema.methods.comparePassword = function(candidatePassword, callback){
    bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
        if(err){ return callback(err); }
        callback(null, isMatch);
    });
}

@aaronfulkerson wie können Sie im Pre-Update-Hook auf this.isModified zugreifen?

Soweit ich Sie verstanden habe, ist isModified nur in der Document API anwendbar, während sich im Pre-Update-Hook this Query stattdessen auf

Ich glaube also nicht, dass es einen anderen Weg gibt, als den, den @ezamelczyk vorgeschlagen hat

@aminnaggar Ich kann mich nicht erinnern. Ich hatte es funktioniert, aber dann habe ich Mongo gegen Postgres ausgetauscht.

Wenn Sie das Hash-Passwort auf getUpdate().$set.password oder getUpdate.password setzen, wird das Passwort weiterhin ungehasht gespeichert. Verwenden Sie anstelle von this.getUpdate this._update. Das hat bei mir funktioniert.

Ich habe dieses Problem mit dem folgenden Code gelöst, hoffe es hilft

userSchema.pre('findOneAndUpdate', async function() {
  const docToUpdate = await this.model.findOne(this.getQuery())

  if (docToUpdate.password !== this._update.password) {
    const newPassword = await hash(this._update.password, 10)
    this._update.password = newPassword
  }
})
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen