Recentemente, eles me deram suporte para um projeto no express.js. Ao estudar o código do projeto, achei um trabalho um pouco confuso com autenticação / autorização, que foi baseado, como 99,999% dos casos, na biblioteca passport.js. Este código funcionou e, seguindo o princípio do "trabalho - não toque", deixei como está. Alguns dias depois, recebi a tarefa de adicionar mais duas estratégias de autorização. E então comecei a lembrar que já estava fazendo um trabalho semelhante, e foram necessárias várias linhas de código. Tendo examinado a documentação do passport.js, quase não me dei conta de como e por que fazer, porque lá eles consideraram casos em que exatamente uma estratégia é usada, para a qual, para cada um individualmente, são dados exemplos. Mas como combinar várias estratégias, por que você precisa usar o método logIn () (que é o mesmo que login ()) - ele ainda não foi esclarecido. Portanto, para entender agora, e não repetir a mesma pesquisa várias vezes, compilei essas anotações para mim.
Um pouco de história. Inicialmente, os aplicativos da web usavam dois tipos de autenticação / autorização: 1) Básico e 2) usando sessões usando cookies. Na autenticação / autorização básica, um cabeçalho é enviado em cada solicitação e, portanto, a autenticação do cliente é realizada em cada solicitação. Ao usar sessões, a autenticação do cliente é realizada apenas uma vez (os métodos podem ser muito diferentes, incluindo Básico, bem como por nome e senha, enviados no formulário, e milhares de outros métodos chamados estratégias em termos de passport.js). O principal é que, após a autenticação, o cliente identifica o identificador da sessão no cookie (ou, em algumas implementações, dados da sessão), e o identificador do usuário é armazenado nos dados da sessão.
Primeiro, você precisa decidir se irá usar sessões no seu aplicativo para autenticação / autorização. Se você estiver desenvolvendo um back-end para um aplicativo móvel, provavelmente não. Se este for um aplicativo da Web, provavelmente sim. Para usar sessões, você precisa ativar o analisador de cookies, o middleware da sessão e também inicializar a sessão:
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());
Aqui você precisa dar algumas explicações importantes. Se você não quiser que os redis consumam toda a memória RAM em alguns anos, precisará cuidar da exclusão oportuna dos dados da sessão. O parâmetro maxAge é responsável por isso, que define igualmente esse valor para o cookie e o valor armazenado em redis. Definir os valores resave: true, rolling: true, estende o período de validade com o valor maxAge especificado a cada nova solicitação (se necessário). Caso contrário, a sessão do cliente será interrompida periodicamente. Por fim, o parâmetro saveUninitialized: false não colocará sessões vazias em redis. Isso permite que você coloque a inicialização das sessões e do passport.js no nível do aplicativo, sem obstruir o redis com dados desnecessários. No nível da rota, faz sentido colocar a inicialização apenas se o método passport.initialize () precisar ser chamado com parâmetros diferentes.
Se a sessão não for usada, a inicialização será significativamente reduzida:
app.use(passport.initialize());
Em seguida, você precisa criar um objeto de estratégia (como passport.js chama o método de autenticação na terminologia). Cada estratégia possui seus próprios recursos de configuração. A única coisa que permanece inalterada é que a função de retorno de chamada é passada para o construtor da estratégia, que forma o objeto do usuário, acessível como request.user para as seguintes filas de 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); }) );
Devemos estar cientes de que, se uma sessão não for usada, esse método será chamado sempre que um recurso protegido for acessado e uma consulta ao banco de dados (como no exemplo) afetará significativamente o desempenho do aplicativo.
Em seguida, você precisa dar um comando para usar a estratégia. Cada estratégia tem um nome padrão. Mas também pode ser definido explicitamente, o que permite o uso de uma estratégia com parâmetros diferentes e a lógica da função de retorno de chamada:
passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy);
Em seguida, para a rota protegida, você precisa definir uma estratégia de autenticação e um parâmetro de sessão importante (o padrão é verdadeiro):
const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); });
Se a sessão não for usada, a autenticação deverá ser protegida por todas as rotas de acesso restrito. Se a sessão for usada, a autenticação ocorrerá uma vez e, para isso, uma rota especial será definida, por exemplo, 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({}); });
Ao usar uma sessão, em rotas protegidas, geralmente é usado um middleware muito conciso (que por algum motivo não está incluído na biblioteca passport.js):
function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); }
Portanto, houve um último momento - serialização e desserialização do objeto request.user de / para a sessão:
passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); });
Quero enfatizar mais uma vez que serialização e desserialização só funcionam com estratégias para as quais o atributo {session: true} está definido. A serialização será realizada exatamente uma vez imediatamente após a autenticação. Portanto, a atualização dos dados armazenados na sessão será muito problemática, em relação à qual apenas o ID do usuário (que não muda) é salvo. A desserialização será realizada em cada solicitação para uma rota segura. Nessa conexão, as consultas ao banco de dados (como no exemplo) afetam significativamente o desempenho do aplicativo.
Observação. Se você usar várias estratégias ao mesmo tempo, o mesmo código de serialização / desserialização funcionará para todas essas estratégias. Para levar em conta a estratégia pela qual a autenticação foi executada, por exemplo, você pode incluir um atributo de estratégia no objeto de usuário. Também não faz sentido chamar o método initialize () várias vezes com valores diferentes. Ele ainda será reescrito com os valores da última chamada.
Este poderia ser o fim da história. Porque além do que foi dito, na prática, nada mais é necessário. No entanto, a pedido dos desenvolvedores de front-end, tive que adicionar um objeto com uma descrição do erro à resposta 401 (por padrão, essa é a linha não autorizada). E isso, como se viu, não pode ser feito simplesmente. Para esses casos, você precisa se aprofundar um pouco mais no núcleo da biblioteca, o que não é tão bom. O método passport.authenticate possui um terceiro parâmetro opcional: uma função de retorno de chamada com a função de assinatura (erro, usuário, informações). O pequeno problema é que nem o objeto de resposta nem qualquer função como done () / next () são passados para essa função e, portanto, você deve convertê-lo em 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); }; }
Links úteis:
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 de janeiro de 2019