As interfaces de programação de aplicativos (APIs) estão em toda parte. Eles permitem que o software se comunique com outras partes do software - interno ou externo - de forma consistente, o que é um ingrediente chave na escalabilidade, sem mencionar a reutilização.
É bastante comum hoje em dia que os serviços online tenham APIs voltadas ao público. Isso permite que outros desenvolvedores integrem facilmente recursos como logins de mídia social, pagamentos com cartão de crédito e rastreamento de comportamento. o de fato o padrão que eles usam para isso é chamado de transferência de estado representacional (REST).
Embora uma grande variedade de plataformas e linguagens de programação possam ser usadas para a tarefa, por exemplo, ASP.NET Core , Laravel (PHP) , ou Garrafa (Python) —Neste tutorial, criaremos um back-end básico, mas seguro, da API REST usando a seguinte pilha:
Os desenvolvedores que seguem este tutorial também devem estar familiarizados com o terminal (ou prompt de comando).
Observação: não cobriremos uma base de código de front-end aqui, mas o fato de que nosso back-end é escrito em JavaScript torna conveniente compartilhar código - modelos de objeto, por exemplo - em toda a pilha.
APIs REST são usados para acessar e manipular dados usando um conjunto comum de operações sem estado. Essas operações são parte integrante do protocolo HTTP e representam a funcionalidade essencial de criar, ler, atualizar e excluir (CRUD), embora não de uma maneira limpa um para um:
POST
(crie um recurso ou geralmente forneça dados)GET
(recuperar um índice de recursos ou um recurso individual)PUT
(criar ou substituir um recurso)PATCH
(atualizar / modificar um recurso)DELETE
(remover um recurso)Usando essas operações HTTP e um nome de recurso como endereço, podemos construir uma API REST criando um terminal para cada operação. E ao implementar o padrão, teremos uma base estável e facilmente compreensível, permitindo-nos evoluir o código rapidamente e mantê-lo depois. Conforme mencionado anteriormente, a mesma base será usada para integrar recursos de terceiros, muitos dos quais também usam APIs REST, tornando essa integração mais rápida.
Por enquanto, vamos começar a criar nossa API REST segura usando Node.js!
Neste tutorial, criaremos uma API REST bastante comum (e muito prática) para um recurso chamado users
.
Nosso recurso terá a seguinte estrutura básica:
id
(um UUID gerado automaticamente)firstName
lastName
email
password
permissionLevel
(o que este usuário tem permissão para fazer?)E criaremos as seguintes operações para esse recurso:
POST
no nó de extremidade /users
(criar um novo usuário)GET
no nó de extremidade /users
(lista todos os usuários)GET
no nó de extremidade /users/:userId
(obter um usuário específico)PATCH
no nó de extremidade /users/:userId
(atualize os dados para um usuário específico)DELETE
no nó de extremidade /users/:userId
(remover um usuário específico)Também usaremos tokens da web JSON (JWTs) para tokens de acesso. Para isso, criaremos outro recurso denominado auth
que irá esperar o e-mail e a senha de um usuário e, em troca, irá gerar o token usado para autenticação em certas operações. (Excelente artigo de Dejan Milosevic sobre JWT para aplicativos REST seguros em Java entra em mais detalhes sobre isso; Os princípios são os mesmos.)
Em primeiro lugar, certifique-se de que possui a versão mais recente do Node.js instalada. Para este artigo, estarei usando a versão 14.9.0; também pode funcionar em versões mais antigas.
Em seguida, certifique-se de que você MongoDB instalado. Não explicaremos os detalhes do Mongoose e do MongoDB que são usados aqui, mas para fazer o básico funcionar, basta iniciar o servidor no modo interativo (ou seja, a partir da linha de comando como mongo
) em vez de como um serviço. Isso ocorre porque, em um ponto neste tutorial, precisaremos interagir com o MongoDB diretamente, em vez de por meio de nosso código Node.js.
Nota: Com o MongoDB, não há necessidade de criar um banco de dados específico como pode haver em alguns cenários RDBMS. A primeira chamada de inserção de nosso código Node.js irá disparar sua criação automaticamente.
Este tutorial não contém todo o código necessário para um projeto de trabalho. Pretende-se, em vez disso, que você clone o repo complementar e simplesmente siga os destaques à medida que lê - mas também pode copiar arquivos e fragmentos específicos do repositório conforme necessário, se preferir.
Navegue até o resultado rest-api-tutorial/
pasta em seu terminal. Você verá que nosso projeto contém três pastas de módulo:
common
(lidar com todos os serviços compartilhados e informações compartilhadas entre os módulos do usuário)users
(tudo sobre usuários)auth
(lidando com a geração de JWT e o fluxo de login)Agora, execute npm install
(ou yarn
se você tiver.)
Parabéns, agora você tem todas as dependências e configurações necessárias para executar nosso back end simples da API REST.
Estaremos usando Mangusto , uma biblioteca de modelagem de dados de objeto (ODM) para MongoDB, para criar o modelo de usuário dentro do esquema de usuário.
Primeiro, precisamos criar o esquema Mongoose em /users/models/users.model.js
:
const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });
Depois de definir o esquema, podemos anexar facilmente o esquema ao modelo de usuário.
const userModel = mongoose.model('Users', userSchema);
Depois disso, podemos usar esse modelo para implementar todas as operações CRUD que desejamos em nossos terminais Express.
a indústria automobilística ainda não implementou a internet das coisas (iot).
Vamos começar com a operação 'criar usuário', definindo a rota em users/routes.config.js
:
app.post('/users', [ UsersController.insert ]);
Isso é colocado em nosso aplicativo Express no principal index.js
Arquivo. O UsersController
objeto é importado de nosso controlador, onde hash a senha apropriadamente, definida em /users/controllers/users.controller.js
:
exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest('base64'); req.body.password = salt + '$' + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };
Neste ponto, podemos testar nosso modelo Mongoose executando o servidor (npm start
) e enviando um POST
solicitação para /users
com alguns dados JSON:
{ 'firstName' : 'Marcos', 'lastName' : 'Silva', 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd' }
Existem várias ferramentas que você pode usar para isso. Insomnia (abordado abaixo) e Postman são ferramentas GUI populares e curl
é uma escolha CLI comum. Você pode até mesmo usar JavaScript, por exemplo, a partir do console de ferramentas de desenvolvimento integrado do seu navegador:
fetch('http://localhost:3600/users', { method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 's3cr3tp4sswo4rd' }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); });
Neste ponto, o resultado de uma postagem válida será apenas o id do usuário criado: { 'id': '5b02c5c84817bf28049e58a3' }
. Precisamos também adicionar o createUser
método para o modelo em users/models/users.model.js
:
exports.createUser = (userData) => { const user = new User(userData); return user.save(); };
Tudo pronto, agora precisamos ver se o usuário existe. Para isso, vamos implementar o recurso “get user by id” para o seguinte endpoint: users/:userId
.
Primeiro, criamos uma rota em /users/routes/config.js
:
app.get('/users/:userId', [ UsersController.getById ]);
Em seguida, criamos o controlador em /users/controllers/users.controller.js
:
exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };
E, finalmente, adicione o findById
método para o modelo em /users/models/users.model.js
:
exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };
A resposta será assim:
{ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }
Observe que podemos ver a senha com hash. Para este tutorial, estamos mostrando a senha, mas a melhor prática óbvia é nunca revelar a senha, mesmo que ela tenha sido hash. Outra coisa que podemos ver é o permissionLevel
, que usaremos para lidar com as permissões do usuário mais tarde.
Repetindo o padrão apresentado acima, podemos agora adicionar a funcionalidade para atualizar o usuário. Usaremos o PATCH
operação, pois nos permitirá enviar apenas os campos que queremos alterar. A rota será, portanto, PATCH
para /users/:userid
, e enviaremos todos os campos que desejamos alterar. Também precisaremos implementar alguma validação extra, uma vez que as alterações devem ser restritas ao usuário em questão ou a um administrador, e apenas um administrador deve ser capaz de alterar o permissionLevel
. Vamos pular isso por agora e voltar a ele assim que implementarmos o módulo de autenticação. Por enquanto, nosso controlador será assim:
exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest('base64'); req.body.password = salt + '$' + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); };
Por padrão, enviaremos um código HTTP 204 sem corpo de resposta para indicar que a solicitação foi bem-sucedida.
E precisaremos adicionar o patchUser
método para o modelo:
exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };
A lista de usuários será implementada como GET
em /users/
pelo seguinte controlador:
exports.list = (req, res) => { let limit = req.query.limit && req.query.limit { res.status(200).send(result); }) };
O método do modelo correspondente será:
exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); };
A resposta da lista resultante terá a seguinte estrutura:
[ { 'firstName': 'Marco', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }, { 'firstName': 'Paulo', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==', 'permissionLevel': 1, 'id': '5b02d038b653603d1ca69729' } ]
E a última parte a ser implementada é o DELETE
em /users/:userId
.
Nosso controlador para exclusão será:
exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };
Da mesma forma que antes, o controlador retornará o código HTTP 204 e nenhum corpo de conteúdo como confirmação.
O método do modelo correspondente deve ser semelhante a este:
exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };
Agora temos todas as operações necessárias para manipular o recurso do usuário e terminamos com o controlador de usuário. A ideia principal deste código é fornecer os principais conceitos de uso do padrão REST. Precisamos retornar a este código para implementar algumas validações e permissões para ele, mas primeiro, precisaremos começar a construir nossa segurança. Vamos criar o módulo de autenticação.
Antes de podermos proteger o users
ao implementar o middleware de permissão e validação, precisaremos ser capazes de gerar um token válido para o usuário atual. Geraremos um JWT em resposta ao usuário fornecer um e-mail e uma senha válidos. JWT é um notável token da web JSON que você pode usar para que o usuário faça várias solicitações com segurança, sem validar repetidamente. Geralmente, ele tem um tempo de expiração e um novo token é recriado a cada poucos minutos para manter a comunicação segura. Para este tutorial, entretanto, deixaremos de atualizar o token e mantê-lo simples com um único token por login.
Primeiro, criaremos um ponto de extremidade para POST
solicitações para /auth
recurso. O corpo da solicitação conterá o e-mail e a senha do usuário:
{ 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd2' }
Antes de envolver o controlador, devemos validar o usuário em /authorization/middlewares/verify.user.middleware.js
:
exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest('base64'); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };
Feito isso, podemos passar para o controlador e gerar o JWT:
exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest('base64'); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };
Mesmo que não atualizemos o token neste tutorial, o controlador foi configurado para permitir tal geração para tornar mais fácil implementá-lo no desenvolvimento subsequente.
Tudo o que precisamos agora é criar a rota e invocar o middleware apropriado em /authorization/routes.config.js
:
app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);
A resposta conterá o JWT gerado no campo accessToken:
{ 'accessToken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY', 'refreshToken': 'U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==' }
Tendo criado o token, podemos usá-lo dentro de Authorization
cabeçalho usando o formulário Bearer ACCESS_TOKEN
.
A primeira coisa que devemos definir é quem pode usar o users
recurso. Estes são os cenários com os quais precisamos lidar:
Tendo identificado esses cenários, primeiro exigiremos um middleware que sempre valide o usuário se ele estiver usando um JWT válido. O middleware em /common/middlewares/auth.validation.middleware.js
pode ser tão simples quanto:
exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };
Usaremos códigos de erro HTTP para lidar com erros de solicitação:
Podemos usar o operador AND bit a bit (bitmasking) para controlar as permissões. Se definirmos cada permissão necessária como uma potência de 2, podemos tratar cada bit do inteiro de 32 bits como uma única permissão. Um administrador pode então ter todas as permissões definindo seu valor de permissão para 2147483647. Esse usuário pode então ter acesso a qualquer rota. Como outro exemplo, um usuário cujo valor de permissão foi definido como 7 teria permissões para as funções marcadas com bits para os valores 1, 2 e 4 (dois elevado a 0, 1 e 2).
O middleware para isso seria assim:
cada parte do conteúdo abordado em aula será em um teste de unidade.
exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };
O middleware é genérico. Se o nível de permissão do usuário e o nível de permissão necessário coincidirem em pelo menos um bit, o resultado será maior que zero e podemos deixar a ação prosseguir; caso contrário, o código HTTP 403 será retornado.
Agora, precisamos adicionar o middleware de autenticação às rotas do módulo do usuário em /users/routes.config.js
:
app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);
Isso conclui o desenvolvimento básico de nossa API REST. Tudo o que resta a fazer é testar tudo.
Insônia é um cliente REST decente com uma boa versão gratuita. A melhor prática é, obviamente, incluir testes de código e implementar relatórios de erros adequados no projeto, mas clientes REST de terceiros são ótimos para testar e implementar soluções de terceiros quando o relatório de erros e depuração do serviço não estão disponíveis. Vamos usá-lo aqui para desempenhar o papel de um aplicativo e obter alguns insights sobre o que está acontecendo com nossa API.
Para criar um usuário, só precisamos POST
os campos obrigatórios para o terminal apropriado e armazenar o ID gerado para uso subsequente.
A API responderá com o ID do usuário:
Agora podemos gerar o JWT usando /auth/
ponto final:
Devemos obter um token como nossa resposta:
Pegue o accessToken
, prefixe-o com Bearer
(lembre-se do espaço) e adicione-o aos cabeçalhos da solicitação em Authorization
:
Se não fizermos isso agora que implementamos o middleware de permissões, todas as solicitações, exceto o registro, retornarão o código HTTP 401. Com o token válido, porém, obtemos a seguinte resposta de /users/:userId
:
Além disso, como foi mencionado antes, estamos exibindo todos os campos, para fins educacionais e por uma questão de simplicidade. A senha (com hash ou não) nunca deve ser visível na resposta.
Vamos tentar obter uma lista de usuários:
Surpresa! Recebemos uma resposta 403.
Nosso usuário não tem permissão para acessar este endpoint. Teremos de alterar o permissionLevel
de nosso usuário de 1 a 7 (ou mesmo 5 serviriam, já que nossos níveis de permissões gratuitas e pagas são representados como 1 e 4, respectivamente). Podemos fazer isso manualmente no MongoDB, em seu prompt interativo, como este (com o ID alterado para o seu resultado local):
db.users.update({'_id' : ObjectId('5b02c5c84817bf28049e58a3')},{$set:{'permissionLevel':5}})
Então, precisamos gerar um novo JWT.
Depois de fazer isso, obtemos a resposta adequada:
A seguir, vamos testar a funcionalidade de atualização enviando um PATCH
solicitação com alguns campos para nosso /users/:userId
ponto final:
Esperamos uma resposta 204 como confirmação de uma operação bem-sucedida, mas podemos solicitar ao usuário uma nova verificação.
Finalmente, precisamos deletar o usuário. Precisamos criar um novo usuário conforme descrito acima (não se esqueça de anotar o ID do usuário) e ter certeza de que temos o JWT apropriado para um usuário administrador. O novo usuário precisará de suas permissões definidas para 2053 (isto é, 2048— ADMIN
—mais nosso 5 anterior) para poder realizar a operação de exclusão. Feito isso e um novo JWT gerado, teremos que atualizar nosso Authorization
cabeçalho do pedido:
Enviando um DELETE
solicitação para /users/:userId
, devemos obter uma resposta 204 como confirmação. Podemos, novamente, verificar solicitando /users/
para listar todos os usuários existentes.
Com as ferramentas e métodos abordados neste tutorial, agora você deve ser capaz de criar APIs REST simples e seguras em Node.js. Muitas práticas recomendadas que não são essenciais para o processo foram ignoradas, então não se esqueça de:
common/config/env.config.js
para um off-repo, não baseado no ambiente mecanismo de distribuição secreta Um exercício final para o leitor pode ser converter a base de código de seu uso de promessas de JavaScript para o assíncrono / esperar técnica.
Para aqueles de vocês que possam estar interessados, agora há também uma versão TypeScript do projeto disponível.
Relacionado: 5 coisas que você nunca fez com uma especificação REST