Baru-baru ini, mereka memberi saya dukungan untuk proyek di express.js. Ketika mempelajari kode proyek, saya menemukan pekerjaan yang membingungkan dengan otentikasi / otorisasi, yang didasarkan, seperti 99,999% kasus, di perpustakaan passport.js. Kode ini berfungsi, dan mengikuti prinsip "kerja - jangan sentuh", saya membiarkannya apa adanya. Ketika beberapa hari kemudian saya diberi tugas untuk menambahkan dua strategi otorisasi lagi. Dan kemudian saya mulai ingat bahwa saya sudah melakukan pekerjaan yang serupa, dan butuh beberapa baris kode. Setelah melihat-lihat dokumentasi tentang passport.js, saya hampir tidak bergeming dalam memahami apa dan bagaimana melakukannya, karena di sana mereka mempertimbangkan kasus-kasus ketika tepat satu strategi digunakan, yang untuk masing-masing individu, contoh diberikan. Tetapi bagaimana menggabungkan beberapa strategi, mengapa Anda perlu menggunakan metode logIn () (yang sama dengan login ()) - masih belum diklarifikasi. Karena itu, untuk memahami sekarang, dan tidak mengulangi pencarian yang sama lagi dan lagi, saya mengkompilasi catatan ini untuk diri saya sendiri.
Sedikit sejarah. Awalnya, aplikasi web menggunakan dua jenis otentikasi / otorisasi: 1) Dasar dan 2) menggunakan sesi menggunakan cookie. Dalam Otentikasi Dasar / Otorisasi, header dikirim dalam setiap permintaan, dan dengan demikian otentikasi klien dilakukan di setiap permintaan. Saat menggunakan sesi, otentikasi klien dilakukan hanya sekali (metode bisa sangat berbeda, termasuk Dasar, serta dengan nama dan kata sandi, yang dikirim dalam bentuk, dan ribuan metode lain yang disebut strategi dalam hal passport.js). Hal utama adalah bahwa setelah otentikasi, klien mengidentifikasi pengidentifikasi sesi dalam cookie (atau, dalam beberapa implementasi, data sesi), dan pengidentifikasi pengguna disimpan dalam data sesi.
Pertama, Anda perlu memutuskan apakah Anda akan menggunakan sesi dalam aplikasi Anda untuk otentikasi / otorisasi. Jika Anda mengembangkan backend untuk aplikasi seluler, maka kemungkinan besar tidak. Jika ini adalah aplikasi web, maka kemungkinan besar ya. Untuk menggunakan sesi, Anda harus mengaktifkan cookie-parser, middleware sesi, dan juga menginisialisasi sesi:
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());
Di sini Anda perlu memberikan beberapa penjelasan penting. Jika Anda tidak ingin redis memakan semua RAM dalam beberapa tahun, Anda perlu mengurus penghapusan data sesi secara tepat waktu. Parameter maxAge bertanggung jawab untuk ini, yang sama-sama menetapkan nilai ini untuk cookie dan nilai yang disimpan dalam redis. Mengatur nilai resave: true, rolling: true, memperpanjang periode validitas dengan nilai maxAge yang ditentukan dengan setiap permintaan baru (jika perlu). Jika tidak, sesi klien akan terputus secara berkala. Akhirnya, saveUninitialized: parameter salah tidak akan menempatkan sesi kosong di redis. Ini memungkinkan Anda untuk menempatkan inisialisasi sesi dan paspor.js di tingkat aplikasi, tanpa menyumbat redis dengan data yang tidak perlu. Pada level rute, masuk akal untuk menempatkan inisialisasi hanya jika metode passport.initialize () perlu dipanggil dengan parameter yang berbeda.
Jika sesi tidak akan digunakan, inisialisasi akan berkurang secara signifikan:
app.use(passport.initialize());
Selanjutnya, Anda perlu membuat objek strategi (seperti passport.js memanggil metode otentikasi dalam terminologi). Setiap strategi memiliki fitur konfigurasi sendiri. Satu-satunya hal yang tetap tidak berubah adalah bahwa fungsi callback diteruskan ke konstruktor strategi, yang membentuk objek pengguna, dapat diakses sebagai request.user untuk antrian middleware berikut:
const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); }) );
Kita harus menyadari bahwa jika suatu sesi tidak digunakan, metode ini akan dipanggil setiap kali sumber daya yang dilindungi diakses, dan permintaan basis data (seperti dalam contoh) akan secara signifikan mempengaruhi kinerja aplikasi.
Selanjutnya, Anda perlu memberikan perintah untuk menggunakan strategi. Setiap strategi memiliki nama default. Tetapi juga dapat diatur secara eksplisit, yang memungkinkan menggunakan satu strategi dengan parameter yang berbeda dan logika fungsi panggilan balik:
passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy);
Selanjutnya, untuk rute yang dilindungi, Anda perlu mengatur strategi otentikasi dan parameter sesi penting (standarnya benar):
const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); });
Jika sesi tidak digunakan, maka otentikasi harus dilindungi oleh semua rute akses terbatas. Jika sesi digunakan, maka otentikasi terjadi sekali, dan untuk ini rute khusus diatur, misalnya login:
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({}); });
Saat menggunakan sesi, pada rute yang dilindungi, sebagai aturan, middleware sangat ringkas (yang karena alasan tertentu tidak termasuk dalam perpustakaan passport.js) digunakan:
function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); }
Jadi, ada satu saat terakhir - serialisasi dan deserialisasi objek request.user ke / dari sesi:
passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); });
Saya ingin menekankan sekali lagi bahwa serialisasi dan deserialisasi hanya berfungsi dengan strategi yang atribut {session: true} ditetapkan. Serialisasi akan dilakukan tepat sekali segera setelah otentikasi. Oleh karena itu, memperbarui data yang disimpan dalam sesi akan sangat bermasalah, sehubungan dengan yang hanya ID pengguna (yang tidak berubah) yang disimpan. Deserialisasi akan dilakukan pada setiap permintaan ke rute yang aman. Dalam hubungan ini, permintaan basis data (seperti dalam contoh) secara signifikan mempengaruhi kinerja aplikasi.
Komentar. Jika Anda menggunakan beberapa strategi sekaligus, kode serialisasi / deserialisasi yang sama akan berfungsi untuk semua strategi ini. Untuk memperhitungkan strategi di mana otentikasi dilakukan, misalnya, Anda dapat memasukkan atribut strategi di objek pengguna. Tidak masuk akal untuk memanggil metode initialize () beberapa kali dengan nilai yang berbeda. Itu masih akan ditulis ulang dengan nilai-nilai dari panggilan terakhir.
Ini bisa menjadi akhir dari cerita. Karena selain apa yang telah dikatakan, dalam praktiknya, tidak ada lagi yang diperlukan. Namun, atas permintaan pengembang front-end, saya harus menambahkan objek dengan deskripsi kesalahan ke respons 401 (secara default, ini adalah baris "Tidak Diotorisasi"). Dan ini, ternyata, tidak bisa dilakukan dengan sederhana. Untuk kasus-kasus seperti itu, Anda perlu sedikit lebih dalam ke inti perpustakaan, yang tidak begitu baik. Metode passport.authenticate memiliki parameter opsional ketiga: fungsi panggilan balik dengan fungsi tanda tangan (kesalahan, pengguna, info). Masalah kecilnya adalah bahwa objek respons atau fungsi apa pun seperti done () / next () tidak diteruskan ke fungsi ini, dan karena itu Anda harus mengubahnya sendiri ke middleware:
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); }; }
Tautan yang bermanfaat:
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 Januari 2019