Немного истории. Изначально веб-приложениях использовали два вида аутентификации/авторизации: 1) BASE и 2) при помощи сессий с использованием cookie. В BASE аутентификации/авторизации в каждом запросе передается некоторый заголовок, и таким образом, в каждом запросе проводится аутентификация клиента. При использовании сессий, аутентифкация клиента проводится только один раз (способы могут быть самые различные в том числе и BASE, а еще по имени и паролю, которые отправляются в форме, и еще тысячи других способов, которые в терминах passport.js называются сратегиями). Главное, что после прохождения аутентификации, клиенту в cookie сохраняется идентификатор сессии (или в некоторых реализациях данные сессии), а в данных сессии сохраняется идентификатор пользователя.
Для начала нужно определиться будете ли Вы использовать в своем приложении сессии при аутентификации/авторизации. Если Вы разрабатываете бэкэнд мобильного приложения — то, скорее всего, нет. Если это веб-приложение — то, скорее всего, да. Для использовани сессий нужно активировать cookie-parser, session middleware, а также инициализировать сессию:
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());
Тут нужно дать несколько важных пояснений. Если Вы не хотите, чтобы redis через пару лет работы съел всю оперативную память, нужно позаботиться о своевременном удалении даных сессии. За это отвечает параметр maxAge, который в равной степени устанавивает это значение и для cookie, и для значения сохраняемого в redis. Установка значений resave: true, rolling: true, продлевает срок действия заданным занчением maxAge при каждом новом запросе (если это нужно). В противном случае сессия клиента будет периодически прерываться. И, наконец, параметр saveUninitialized: false не будет помещать в redis пустые сессии. Это позволяет разместить инициализацию сессий и passport.js на уровне приложения, не засоряя redis лишними данными. На уровне роутов инициализацию имеет смысл размещать только в том сучае, если метод passport.initialize() необходимо вызывать с разными параметрами.
Если сессия не будет использоваться то иницализация значительно сократится:
app.use(passport.initialize());
Далее нужно создать объект стратегии (так в терминологии passport.js называют способ аутентификации). Каждая стратегия имеет свои особенности конфигрурирования. Неизменным сотается только то, что в конструктор стратегии передается callback-функция, которая формирует объект user, доступный как request.user для следующих в очереди middleware:
const jwtStrategy = new JwtStrategy(params, (payload, done) =>
UserModel.findOne({where: {id: payload.userId}})
.then((user = null) => {
done(null, user);
})
.catch((error) => {
done(error, null);
})
);
Надо хорошо представлять себе, что если не используется сессия, это метод будет вызываться при каждом обращении к защищенному ресурсу, и запрос к базе данных (как в примере) существенно скажется на производительности приложения.
Далее нужно дать команду на использование стратегии. Каждая стратегия имеет имя по умолчанию. Но его можно задать и явно, что позволяет использовать одну стратегию с разными параметрами и логикой callback-функции:
passport.use('jwt', jwtStrategy);
passport.use('simple-jwt', simpleJwtStrategy);
Далее для защищаемого роута необходимо задать стратегию аутентификации и важный параметр session (по умочанию равный true):
const authenticate = passport.authenticate('jwt', {session: false});
router.use('/hello', authenticate, (req, res) => {
res.send('hello');
});
Если сессия не используется, то защищать аутентификацией нужно все роуты с ограниченным доступом. Если же сессия используется, то аутентификация происходит однократно, и для этого задается специальный роут, например 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({});
});
На защищаемых роутах, как правило, используется очень лаконичное middleware (которое почему-то не включено в библиотеку passport.js):
function mustAuthenticated(req, res, next) {
if (!req.isAuthenticated()) {
return res.status(HTTPStatus.UNAUTHORIZED).send({});
}
next();
}
Итак, остался один последний момент — сериализация и десериализация объекта request.user в сессию/из сессии:
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
UserModel.findOne({where: {id}}).then((user) => {
done(null, user);
return null;
});
});
Сериализация будет выполнена ровно один раз сразу после аутентификации. Поэтому обновить данные сохраненные в сессии будет весьма пробематично, в связи с чем сохраняется тоько идентификатор пользователя (который не меняется). Десериализация будет выполняться при каждом запросе к защищенному роуту. В связи с чем запросы к базе данных (как в примере) существенно влияют на производительность приложения.
На этом можно было бы и закончить повествование. Т.к. кроме уже сказанного, на практике ничего другого не требуется. Однако, мне пришлось по требованию разработчиков фронтенда добавить в 401 ответ объект с описанием ошибки. И это, как оказалось, не получается сделать просто. Для таких случаев нужно еще немного глубже залезть в ядро билиотеки, что не так уж и приятно. У метода passport.authenticate есть третий опциональный параметр: callback-функция с сигнатурой function(error, user, info). Небольшая проблема заключается в том, что этой функции не передается ни объект response, ни какaя-нибудь функция типа done()/next(), в связи с чем приходится самостоятельно преобразовывать ее в middleware:
route.post('/login', 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;
return next();
})(req, res, next);
};
}
Полезные ссылки:
1) toon.io/understanding-passportjs-authentication-flow
2) habr.com/post/201206
3) habr.com/company/ruvds/blog/335434
4) habr.com/post/262979
apapacy@gmail.com
4 января 2019 года
Комментариев нет:
Отправить комментарий