RBAC na prática: Implementando autorização segura para seu aplicativo
Você está tendo dificuldades para implementar um sistema de autorização seguro e escalável para seu aplicativo? O Controle de Acesso Baseado em Papel (RBAC) é o padrão da indústria para gerenciar permissões de usuários, mas implementá-lo corretamente pode ser desafiador. Este tutorial mostrará como construir um sistema RBAC robusto usando um exemplo real de Sistema de Gerenciamento de Conteúdo (CMS).
Ao seguir este guia, você aprenderá:
- ✨ Como desenhar e implementar permissões granulares que oferecem controle preciso
- 🔒 Melhores práticas para organizar permissões em papéis significativos
- 👤 Técnicas para lidar com propriedade de recursos de forma eficaz
- 🚀 Maneiras de tornar seu sistema de autorização escalável e fácil de manter
- 💡 Implementação prática usando um exemplo real de CMS
O código-fonte completo deste tutorial está disponível no GitHub.
Entendendo os fundamentos do RBAC
O Controle de Acesso Baseado em Papel vai além de apenas atribuir permissões aos usuários. Trata-se de criar uma abordagem estruturada para autorização que equilibra segurança com facilidade de manutenção.
Você pode saber mais sobre O que é RBAC na Auth Wiki.
Aqui estão os princípios-chave que seguiremos em nossa implementação:
Design de permissões granulares
Permissões granulares oferecem controle preciso sobre o que os usuários podem fazer em seu sistema. Em vez de níveis amplos de acesso como "admin" ou "usuário", definimos ações específicas que os usuários podem executar em recursos. Por exemplo:
read:articles
- Visualizar qualquer artigo no sistemacreate:articles
- Criar novos artigosupdate:articles
- Modificar artigos existentespublish:articles
- Alterar o status de publicação dos artigos
Propriedade de recursos e controle de acesso
A propriedade de recursos é um conceito fundamental no design de autorização do nosso CMS. Enquanto o RBAC define quais ações diferentes papéis podem executar, a propriedade adiciona uma dimensão pessoal ao controle de acesso:
- Autores têm acesso automaticamente aos artigos que criaram
- Esse modelo natural de propriedade significa que autores sempre podem visualizar e editar seu próprio conteúdo
- O sistema verifica tanto as permissões do papel OU a propriedade ao lidar com operações em artigos
- Por exemplo, mesmo sem a permissão
update:articles
, um autor ainda pode editar seus próprios artigos - Esse design reduz a necessidade de permissões extras de papel, mantendo a segurança
Essa abordagem de duas camadas (papéis + propriedade) cria um sistema mais intuitivo e seguro. Editores e administradores ainda podem gerenciar todo o conteúdo por meio das permissões de seus papéis, enquanto autores mantêm o controle sobre seu próprio trabalho.
Desenhando APIs seguras
Vamos começar desenhando a funcionalidade principal do nosso CMS através de seus endpoints de API:
GET /api/articles # Listar todos os artigos
GET /api/articles/:id # Obter um artigo específico
POST /api/articles # Criar um novo artigo
PATCH /api/articles/:id # Atualizar um artigo
DELETE /api/articles/:id # Excluir um artigo
PATCH /api/articles/:id/published # Alterar status de publicação
Implemente controle de acesso para sua API
Para cada endpoint, precisamos considerar dois aspectos do controle de acesso:
- Propriedade do recurso - O usuário é dono deste recurso?
- Permissões baseadas em papel - O papel do usuário permite esta operação?
Veja como lidaremos com o acesso para cada endpoint:
Endpoint | Lógica de controle de acesso |
---|---|
GET /api/articles | - Qualquer um com permissão list:articles , OU autores podem ver seus próprios artigos |
GET /api/articles/:id | - Qualquer um com permissão read:articles , OU autor do artigo |
POST /api/articles | - Qualquer um com permissão create:articles |
PATCH /api/articles/:id | - Qualquer um com permissão update:articles , OU autor do artigo |
DELETE /api/articles/:id | - Qualquer um com permissão delete:articles , OU autor do artigo |
PATCH /api/articles/:id/published | - Apenas usuários com permissão publish:articles |
Criando um sistema de permissões escalável
Com base nos requisitos de acesso à API, podemos definir estas permissões:
Permissão | Descrição |
---|---|
list:articles | Visualizar a lista de todos os artigos no sistema |
read:articles | Ler o conteúdo completo de qualquer artigo |
create:articles | Criar novos artigos |
update:articles | Modificar qualquer artigo |
delete:articles | Excluir qualquer artigo |
publish:articles | Alterar status de publicação |
Observe que essas permissões só são necessárias ao acessar recursos que você não possui. Os proprietários dos artigos podem automaticamente:
- Visualizar seus próprios artigos (não precisa de
read:articles
) - Editar seus próprios artigos (não precisa de
update:articles
) - Excluir seus próprios artigos (não precisa de
delete:articles
)
Construindo papéis eficazes
Agora que temos nossa API e permissões definidas, podemos criar papéis que agrupam essas permissões logicamente:
Permissão/Papel | 👑 Admin | 📝 Editor | ✍️ Autor |
---|---|---|---|
Descrição | Acesso total ao sistema para gerenciamento completo | Pode ver todos os artigos e controlar publicação | Pode criar novos artigos no sistema |
list:articles | ✅ | ✅ | ❌ |
read:articles | ✅ | ✅ | ❌ |
create:articles | ✅ | ❌ | ✅ |
update:articles | ✅ | ❌ | ❌ |
delete:articles | ✅ | ❌ | ❌ |
publish:articles | ✅ | ✅ | ❌ |
Nota: Autores têm automaticamente permissões de leitura/atualização/exclusão para seus próprios artigos, independentemente das permissões do papel.
Cada papel é desenhado com responsabilidades específicas em mente:
- Admin: Tem controle total sobre o CMS, incluindo todas as operações em artigos
- Editor: Foca na revisão de conteúdo e gerenciamento de publicação
- Autor: Especialista em criação de conteúdo
Essa estrutura de papéis cria uma separação clara de responsabilidades:
- Autores focam na criação de conteúdo
- Editores gerenciam a qualidade e visibilidade do conteúdo
- Admins mantêm o controle geral do sistema
Configurando RBAC no Logto
Antes de começar, você precisa criar uma conta no Logto Cloud, ou também pode usar uma instância Logto auto-hospedada utilizando a versão Logto OSS.
Mas para este tutorial, usaremos o Logto Cloud pela simplicidade.
Configurando seu aplicativo
- Vá em "Applications" no Logto Console para criar um novo aplicativo react
- Nome do aplicativo: Sistema de Gerenciamento de Conteúdo
- Tipo de aplicativo: Aplicativo Web Tradicional
- Redirect URIs: http://localhost:5173/callback
Configurando recursos de API e permissões
- Vá em "API Resources" no Logto Console para criar um novo recurso de API
- Nome da API: CMS API
- Identificador da API: https://api.cms.com
- Adicione permissões ao recurso de API
list:articles
read:articles
create:articles
update:articles
publish:articles
delete:articles
Criando papéis
Vá em Roles no Logto Console para criar os seguintes papéis para o CMS
- Admin
- com todas as permissões
- Editor
- com
read:articles
,list:articles
,publish:articles
- com
- Autor
- com
create:articles
- com
Atribuindo papéis aos usuários
Vá até a seção "User management" no Logto Console para criar usuários.
Na aba "Roles" dos detalhes do usuário, você pode atribuir papéis ao usuário.
No nosso exemplo, criamos 3 usuários com os seguintes papéis:
- Alex: Admin
- Bob: Editor
- Charlie: Autor
Para fins de demonstração, criamos esses recursos e configurações pelo Logto Console. Em projetos reais, você pode criar esses recursos e configurações programaticamente usando a Management API fornecida pelo Logto.
Integre seu frontend com o RBAC do Logto
Agora que configuramos o RBAC no Logto, podemos começar a integrá-lo ao nosso frontend.
Primeiro, siga os Logto Quick Starts para integrar o Logto ao seu aplicativo.
No nosso exemplo, usamos React para demonstração.
Depois de configurar o Logto em seu aplicativo, precisamos adicionar as configurações de RBAC para o Logto funcionar.
// frontend/src/App.tsx
const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // O app ID que você criou no Logto Console
endpoint: LOGTO_ENDPOINT, // O endpoint que você criou no Logto Console
resources: [API_RESOURCE], // O identificador do recurso de API criado no Logto Console, ex: https://api.cms.com
// Todos os escopos que você pode querer requisitar do recurso de API no frontend
scopes: [
'list:articles',
'create:articles',
'read:articles',
'update:articles',
'delete:articles',
'publish:articles',
],
};
Lembre-se de sair e entrar novamente para que essa alteração tenha efeito se você já estiver autenticado.
Quando o usuário faz login com o Logto e solicita um token de acesso para os recursos de API especificados acima, o Logto adicionará os escopos (permissões) relacionados ao papel do usuário ao token de acesso.
Você pode usar getAccessTokenClaims
do hook useLogto
para obter os escopos do token de acesso.
// frontend/src/hooks/use-user-data.ts
import { useLogto } from '@logto/react';
import { API_RESOURCE } from '../config';
import { useState, useEffect } from 'react';
export const useUserData = () => {
const { getAccessTokenClaims } = useLogto();
const [userScopes, setUserScopes] = useState<string[]>([]);
const [userId, setUserId] = useState<string>();
useEffect(() => {
const fetchScopes = async () => {
const token = await getAccessTokenClaims(API_RESOURCE);
setUserScopes(token?.scope?.split(' ') ?? []);
setUserId(token?.sub);
};
fetchScopes();
}, [getAccessTokenClaims]);
return { userId, userScopes };
};
E você pode usar o userScopes
para verificar se o usuário tem permissão para acessar o recurso.
// frontend/src/pages/Dashboard.tsx
const Dashboard = () => {
const { userId, userScopes } = useUserData();
// ...
return (
<div>
{/* ... */}
{(userScopes.includes('delete:articles') || article.ownerId === userId) && (
<button
onClick={() => handleDelete(article.id)}
className="text-red-600 hover:text-red-900"
>
Excluir
</button>
)}
</div>
);
};
Integre seu backend com o RBAC do Logto
Agora, é hora de integrar o RBAC do Logto ao seu backend.
Middleware de autorização no backend
Primeiro, precisamos adicionar um middleware no backend para verificar as permissões do usuário, conferir se o usuário está autenticado e determinar se ele tem as permissões necessárias para acessar certas APIs.
// backend/src/middleware/auth.js
const { createRemoteJWKSet, jwtVerify } = require('jose');
const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';
if (!authorization) {
throw new Error('Authorization header missing');
}
if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}
return authorization.slice(bearerTokenIdentifier.length + 1);
};
const hasScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const verifyJwt = async (token) => {
const JWKS = createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL));
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.LOGTO_ISSUER,
audience: process.env.LOGTO_API_RESOURCE,
});
return payload;
};
const requireAuth = (requiredScopes = []) => {
return async (req, res, next) => {
try {
// Extrai o token
const token = getTokenFromHeader(req.headers);
// Verifica o token
const payload = await verifyJwt(token);
// Adiciona informações do usuário à requisição
req.user = {
id: payload.sub,
scopes: payload.scope?.split(' ') || [],
};
// Verifica os escopos necessários
if (!hasScopes(req.user.scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
hasScopes,
};
Como você pode ver, neste middleware, verificamos se a requisição do frontend contém um token de acesso válido e conferimos se o público do token de acesso corresponde ao recurso de API que criamos no Logto Console.
O motivo de verificar o recurso de API é que nosso recurso de API representa, de fato, os recursos do backend do nosso CMS, e todas as permissões do CMS estão associadas a esse recurso de API.
Como esse recurso de API representa os recursos do CMS no Logto, em nosso código frontend, incluímos o respectivo token de acesso ao fazer requisições de API para o backend:
// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();
return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// Obtém o token de acesso para o recurso de API
const token = await getAccessToken(API_RESOURCE);
if (!token) {
throw new ApiRequestError('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
// Adiciona o token de acesso ao cabeçalho da requisição
Authorization: `Bearer ${token}`,
...options.headers,
},
});
// ... trata resposta
return await response.json();
} catch (error) {
// ... tratamento de erro
}
},
[getAccessToken]
);
};
Agora podemos usar o middleware requireAuth
para proteger nossos endpoints de API.
Protegendo endpoints de API
Para APIs que devem ser acessíveis apenas a usuários com permissões específicas, podemos adicionar restrições diretamente no middleware. Por exemplo, a API de criação de artigos deve ser acessível apenas a usuários com a permissão create:articles
:
// backend/src/routes/articles.js
const { requireAuth } = require('../middleware/auth');
router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});
Para APIs que precisam verificar tanto permissões quanto propriedade do recurso, podemos usar a função hasScopes
. Por exemplo, na API de listagem de artigos, usuários com o escopo list:articles
podem acessar todos os artigos, enquanto autores podem acessar apenas os artigos que criaram:
// backend/src/routes/articles.js
const { requireAuth, hasScopes } = require('../middleware/auth');
router.get('/articles', requireAuth(), async (req, res) => {
try {
// Se o usuário tem o escopo list:articles, retorna todos os artigos
if (hasScopes(req.user.scopes, ['list:articles'])) {
const articles = await articleDB.list();
return res.json(articles);
}
// Caso contrário, retorna apenas os artigos do usuário
const articles = await articleDB.listByOwner(req.user.id);
res.json(articles);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch articles' });
}
});
Neste ponto, concluímos a implementação do RBAC. Você pode conferir o código-fonte completo para ver a implementação completa.
Testando a implementação do RBAC no CMS
Agora, vamos testar nossa implementação do RBAC no CMS usando os três usuários que acabamos de criar.
Se você perceber que não consegue fazer login com as credenciais dos usuários criados em "User Management", será necessário habilitar o método de login apropriado primeiro. Vá em "Sign-in Experience" no Logto Console e habilite seu método de autenticação preferido (como Email + Senha ou Nome de usuário + Senha).
Primeiro, vamos fazer login como Alex e Charles, respectivamente, e criar alguns artigos.
Como Alex tem o papel de Admin, ele pode criar, excluir, atualizar, publicar e visualizar todos os artigos.
Charles, com o papel de Autor, só pode criar seus próprios artigos e pode apenas visualizar, atualizar e excluir artigos que ele mesmo criou.
Bob, com o papel de Editor, pode visualizar e publicar todos os artigos, mas não pode criar, atualizar ou excluir.
Conclusão
Parabéns! Você aprendeu como implementar um sistema RBAC robusto em seu aplicativo.
Para cenários mais complexos, como construir aplicativos multi-inquilino, o Logto oferece suporte abrangente a organizações. Confira nosso guia Construa um aplicativo SaaS multi-inquilino: Um guia completo do design à implementação para saber mais sobre como implementar controle de acesso em toda a organização.
Boas codificações! 🚀