Kürzlich haben sie mich bei einem Projekt auf express.js unterstützt. Beim Studium des Projektcodes fand ich eine etwas verwirrende Arbeit mit der Authentifizierung / Autorisierung, die wie 99,999% der Fälle auf der Bibliothek passport.js basierte. Dieser Code hat funktioniert, und nach dem Prinzip "Arbeit - nicht anfassen" habe ich ihn unverändert gelassen. Als ich ein paar Tage später die Aufgabe erhielt, zwei weitere Autorisierungsstrategien hinzuzufügen. Und dann begann ich mich daran zu erinnern, dass ich bereits einen ähnlichen Job machte und mehrere Codezeilen benötigte. Nachdem ich die Dokumentation auf passport.js durchgesehen hatte, rührte ich mich fast nicht, was und wie zu tun ist, weil Dort betrachteten sie Fälle, in denen genau eine Strategie angewendet wird, für die für jede Person Beispiele gegeben werden. Aber wie man mehrere Strategien kombiniert, warum man die logIn () -Methode verwenden muss (die mit login () identisch ist) - es wurde noch nicht geklärt. Um dies jetzt zu verstehen und nicht immer wieder dieselbe Suche zu wiederholen, habe ich diese Notizen für mich zusammengestellt.
Ein bisschen Geschichte. Anfänglich verwendeten Webanwendungen zwei Arten der Authentifizierung / Autorisierung: 1) Basic und 2) Sitzungen mit Cookies. Bei der Standardauthentifizierung / -autorisierung wird in jeder Anforderung ein Header gesendet, und daher wird in jeder Anforderung eine Clientauthentifizierung durchgeführt. Bei Verwendung von Sitzungen wird die Clientauthentifizierung nur einmal durchgeführt (die Methoden können sehr unterschiedlich sein, einschließlich Basic sowie Name und Kennwort, die im Formular gesendet werden, und Tausende anderer Methoden, die in Bezug auf passport.js als Strategien bezeichnet werden). Die Hauptsache ist, dass der Client nach der Authentifizierung die Sitzungskennung im Cookie (oder in einigen Implementierungen die Sitzungsdaten) identifiziert und die Benutzerkennung in den Sitzungsdaten gespeichert wird.
Zunächst müssen Sie entscheiden, ob Sie Sitzungen in Ihrer Anwendung zur Authentifizierung / Autorisierung verwenden möchten. Wenn Sie ein Backend für eine mobile Anwendung entwickeln, ist dies höchstwahrscheinlich nicht der Fall. Wenn dies eine Webanwendung ist, dann höchstwahrscheinlich ja. Um Sitzungen verwenden zu können, müssen Sie den Cookie-Parser und die Sitzungs-Middleware aktivieren und die Sitzung initialisieren:
const app = express(); const sessionMiddleware = session({ store: new RedisStore({client: redisClient}), secret, resave: true, rolling: true, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000, httpOnly: false, }, }); app.use(cookieParser()); app.use(sessionMiddleware); app.use(passport.initialize()); app.use(passport.session());
Hier müssen Sie einige wichtige Erklärungen geben. Wenn Sie nicht möchten, dass Redis in ein paar Jahren den gesamten Arbeitsspeicher verbraucht, müssen Sie sich um das rechtzeitige Löschen der Sitzungsdaten kümmern. Verantwortlich dafür ist der Parameter maxAge, der diesen Wert sowohl für das Cookie als auch für den in redis gespeicherten Wert gleichermaßen festlegt. Durch Festlegen der Werte resave: true, running: true wird die Gültigkeitsdauer bei jeder neuen Anforderung um den angegebenen maxAge-Wert verlängert (falls erforderlich). Andernfalls wird die Client-Sitzung regelmäßig unterbrochen. Schließlich werden mit dem Parameter saveUninitialized: false keine leeren Sitzungen in redis platziert. Auf diese Weise können Sie die Initialisierung von Sitzungen und passport.js auf Anwendungsebene platzieren, ohne Redis mit unnötigen Daten zu verstopfen. Auf Routenebene ist es sinnvoll, die Initialisierung nur dann durchzuführen, wenn die Methode passport.initialize () mit unterschiedlichen Parametern aufgerufen werden muss.
Wenn die Sitzung nicht verwendet wird, wird die Initialisierung erheblich reduziert:
app.use(passport.initialize());
Als Nächstes müssen Sie ein Strategieobjekt erstellen (da passport.js die Authentifizierungsmethode in der Terminologie aufruft). Jede Strategie hat ihre eigenen Konfigurationsfunktionen. Das einzige, was unverändert bleibt, ist, dass die Rückruffunktion an den Strategiekonstruktor übergeben wird, der das Benutzerobjekt bildet, auf das als request.user für die folgenden Middleware-Warteschlangen zugegriffen werden kann:
const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); }) );
Wir müssen uns bewusst sein, dass diese Methode jedes Mal aufgerufen wird, wenn auf eine geschützte Ressource zugegriffen wird, wenn eine Sitzung nicht verwendet wird, und dass eine Datenbankabfrage (wie im Beispiel) die Anwendungsleistung erheblich beeinträchtigt.
Als nächstes müssen Sie einen Befehl geben, um die Strategie zu verwenden. Jede Strategie hat einen Standardnamen. Es kann aber auch explizit festgelegt werden, wodurch eine Strategie mit unterschiedlichen Parametern und der Logik der Rückruffunktion verwendet werden kann:
passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy);
Als Nächstes müssen Sie für die geschützte Route eine Authentifizierungsstrategie und einen wichtigen Sitzungsparameter festlegen (Standard ist true):
const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); });
Wenn die Sitzung nicht verwendet wird, muss die Authentifizierung durch alle eingeschränkten Zugriffswege geschützt werden. Wenn die Sitzung verwendet wird, erfolgt die Authentifizierung einmal, und hierfür wird eine spezielle Route festgelegt, z. B. Anmeldung:
const authenticate = passport.authenticate('local', {session: true}); router.post('/login', authenticate, (req, res) => { res.send({}) ; }); router.post('/logout', mustAuthenticated, (req, res) => { req.logOut(); res.send({}); });
Bei Verwendung einer Sitzung auf geschützten Routen wird in der Regel eine sehr präzise Middleware verwendet (die aus irgendeinem Grund nicht in der Bibliothek passport.js enthalten ist):
function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); }
Es gab also einen letzten Moment - Serialisierung und Deserialisierung des request.user-Objekts zur / von der Sitzung:
passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); });
Ich möchte noch einmal betonen, dass Serialisierung und Deserialisierung nur mit Strategien funktionieren, für die das Attribut {session: true} festgelegt ist. Die Serialisierung wird unmittelbar nach der Authentifizierung genau einmal durchgeführt. Daher ist das Aktualisieren der in der Sitzung gespeicherten Daten sehr problematisch, in Verbindung damit nur die Benutzer-ID (die sich nicht ändert) gespeichert wird. Die Deserialisierung wird bei jeder Anforderung an eine sichere Route durchgeführt. In diesem Zusammenhang wirken sich Datenbankabfragen (wie im Beispiel) erheblich auf die Anwendungsleistung aus.
Bemerkung. Wenn Sie mehrere Strategien gleichzeitig verwenden, funktioniert für alle diese Strategien derselbe Serialisierungs- / Deserialisierungscode. Um beispielsweise die Strategie zu berücksichtigen, mit der die Authentifizierung durchgeführt wurde, können Sie dem Benutzerobjekt ein Strategieattribut hinzufügen. Es macht auch keinen Sinn, die Methode initialize () mehrmals mit unterschiedlichen Werten aufzurufen. Es wird weiterhin mit den Werten des letzten Aufrufs neu geschrieben.
Dies könnte das Ende der Geschichte sein. Weil Zusätzlich zu dem, was gesagt wurde, ist in der Praxis nichts anderes erforderlich. Auf Anfrage der Front-End-Entwickler musste ich der 401-Antwort jedoch ein Objekt mit einer Beschreibung des Fehlers hinzufügen (standardmäßig ist dies die Zeile "Nicht autorisiert"). Und das kann, wie sich herausstellte, nicht einfach gemacht werden. In solchen Fällen müssen Sie etwas tiefer in den Kern der Bibliothek vordringen, was nicht so schön ist. Die Methode passport.authenticate verfügt über einen dritten optionalen Parameter: eine Rückruffunktion mit der Signaturfunktion (Fehler, Benutzer, Info). Das kleine Problem ist, dass weder das Antwortobjekt noch eine Funktion wie done () / next () an diese Funktion übergeben wird und Sie sie daher selbst in Middleware konvertieren müssen:
route.post('/hello', authenticate('jwt', {session: false}), (req, res) => { res.send({}) ; }); function authenticate(strategy, options) { return function (req, res, next) { passport.authenticate(strategy, options, (error, user , info) => { if (error) { return next(error); } if (!user) { return next(new TranslatableError('unauthorised', HTTPStatus.UNAUTHORIZED)); } if (options.session) { return req.logIn(user, (err) => { if (err) { return next(err); } return next(); }); } req.user = user; next(); })(req, res, next); }; }
Nützliche Links:
1)
toon.io/understanding-passportjs-authentication-flow2)
habr.com/post/2012063)
habr.com/company/ruvds/blog/3354344)
habr.com/post/2629795)
habr.com/company/Voximplant/blog/3231606)
habr.com/company/dataart/blog/2628177)
tools.ietf.org/html/draft-ietf-oauth-pop-architecture-088)
oauth.net/articles/authenticationapapacy@gmail.com
4. Januar 2019