Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Proteção por papéis (roles) e permissões por rota e por componente

Capítulo 10

Tempo estimado de leitura: 10 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Em uma SPA, autenticar o usuário (saber “quem é”) é apenas metade do trabalho. A outra metade é autorizar (saber “o que pode fazer”). Proteção por papéis (roles) e permissões é a camada que impede que um usuário autenticado acesse rotas, páginas e ações que não pertencem ao seu perfil. Nesta seção, vamos tratar de autorização em dois níveis: por rota (navegação) e por componente (UI e ações pontuais), com um modelo prático e escalável.

Conceitos: roles, permissões e políticas

Role (papel) é um rótulo de perfil, como admin, manager, support, user. Roles são úteis para regras amplas: “admins podem acessar a área administrativa”.

Permissão é uma capacidade mais granular, geralmente ligada a um recurso e uma ação: users:read, users:write, billing:manage, reports:view. Permissões são úteis para regras finas: “pode editar usuário, mas não pode deletar”.

Política (policy) é uma regra de autorização expressa como uma função/condição: “pode editar se for admin OU se for o dono do recurso”. Políticas frequentemente precisam de contexto (ex.: userId do item que está sendo editado).

Na prática, você pode usar apenas roles, apenas permissões, ou uma combinação. Um padrão comum é: roles definem “macro-acessos” (rotas e áreas), permissões definem “micro-acessos” (botões, ações, campos), e políticas resolvem casos condicionais (ownership, status do recurso, etc.).

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Modelo de dados de autorização no front-end

O front-end não deve ser a fonte de verdade de autorização (isso é do back-end), mas precisa de um espelho para controlar UI e navegação. Um modelo simples e prático:

  • roles: lista de strings
  • permissions: lista de strings (ou um Set)
  • claims adicionais: por exemplo tenantId, orgId, features (flags), etc.

Exemplo de payload (vindo do back-end via endpoint “me” ou claims do token):

type Authz = {  roles: string[];  permissions: string[];};

Para facilitar checagens, é comum normalizar permissões em um Set:

const permissionsSet = new Set(auth.permissions);

Isso evita array.includes repetido e melhora legibilidade.

Estratégia: autorização por rota vs por componente

Por rota (route-level)

O objetivo é impedir que o usuário navegue para uma rota que não deveria ver. Isso reduz exposição de telas e evita que o usuário “descubra” páginas via URL. A proteção por rota normalmente acontece no nível do React Router, usando wrappers/guards específicos para autorização (não apenas autenticação).

Por componente (component-level)

Mesmo dentro de uma rota permitida, pode haver ações restritas: botões de “Excluir”, “Aprovar”, “Exportar CSV”, campos editáveis, etc. A proteção por componente evita que a UI ofereça ações indevidas e ajuda a manter consistência visual e de UX.

Importante: esconder um botão não é segurança. O back-end deve validar permissões ao executar a ação. O front-end faz controle de UI e navegação, mas o servidor é quem bloqueia de verdade.

Passo a passo: criando utilitários de autorização (hooks e helpers)

Assumindo que você já possui um estado de autenticação que expõe o usuário e seus dados (incluindo roles/permissões), crie um hook dedicado para autorização. A ideia é centralizar a lógica e evitar espalhar if (roles.includes(...)) pela aplicação.

1) Defina tipos e convenções de permissões

Escolha um padrão consistente para permissões. Exemplos comuns:

  • resource:action (ex.: users:read, users:delete)
  • resource.action (ex.: users.read)
  • scope estilo OAuth (ex.: read:users)

O mais importante é consistência e facilidade de leitura.

2) Implemente funções de checagem

Crie helpers para: “tem role?”, “tem permissão?”, “tem todas?”, “tem alguma?”.

type CheckMode = 'any' | 'all';type AuthzData = {  roles: string[];  permissions: string[];};function hasRole(authz: AuthzData | null, role: string) {  if (!authz) return false;  return authz.roles.includes(role);}function hasPermission(authz: AuthzData | null, perm: string) {  if (!authz) return false;  return authz.permissions.includes(perm);}function hasPermissions(authz: AuthzData | null, perms: string[], mode: CheckMode = 'all') {  if (!authz) return false;  if (perms.length === 0) return true;  const set = new Set(authz.permissions);  if (mode === 'all') return perms.every(p => set.has(p));  return perms.some(p => set.has(p));}

Essas funções devem ser puras e fáceis de testar.

3) Crie um hook useAuthorization

O hook encapsula o acesso ao estado atual e expõe uma API amigável.

function useAuthorization() {  // Exemplo: pegue do seu contexto/hook de auth já existente  const auth = useAuth();  const authz: AuthzData | null = auth.user    ? { roles: auth.user.roles ?? [], permissions: auth.user.permissions ?? [] }    : null;  return {    roles: authz?.roles ?? [],    permissions: authz?.permissions ?? [],    hasRole: (role: string) => hasRole(authz, role),    hasPermission: (perm: string) => hasPermission(authz, perm),    hasPermissions: (perms: string[], mode: CheckMode = 'all') => hasPermissions(authz, perms, mode),  };}

Se o seu usuário ainda está carregando (ex.: requisição “me”), você pode expor também um isLoading e tratar isso nas guards para evitar “flicker” (mostrar e esconder rapidamente).

Proteção por rota com roles e permissões

Para proteger rotas, você pode criar um componente de guarda que recebe requisitos e decide entre: renderizar o conteúdo, redirecionar para “não autorizado”, ou redirecionar para login (se não autenticado).

1) Defina uma rota de “não autorizado”

Tenha uma página simples para casos em que o usuário está logado, mas não tem acesso. Isso melhora UX e evita loops de redirecionamento.

// Exemplo de rota// /unauthorized - página informativa

2) Crie um guard de autorização

Um guard típico recebe requiredRoles e/ou requiredPermissions. Você pode permitir que qualquer um dos critérios seja suficiente, ou exigir ambos. Uma abordagem clara é: se requiredRoles existir, precisa cumprir; se requiredPermissions existir, precisa cumprir. Assim, você compõe requisitos.

import { Navigate, Outlet, useLocation } from 'react-router-dom';type AuthorizationGuardProps = {  requiredRoles?: string[];  requiredPermissions?: string[];  permissionsMode?: 'any' | 'all';  redirectTo?: string;};function AuthorizationGuard({  requiredRoles = [],  requiredPermissions = [],  permissionsMode = 'all',  redirectTo = '/unauthorized',}: AuthorizationGuardProps) {  const location = useLocation();  const auth = useAuth();  const { hasPermissions } = useAuthorization();  // Se não autenticado, mande para login (mantendo destino)  if (!auth.isAuthenticated) {    return <Navigate to="/login" state={{ from: location }} replace />;  }  // Checa roles  const rolesOk = requiredRoles.length === 0    ? true    : requiredRoles.some(r => auth.user?.roles?.includes(r));  // Checa permissões  const permsOk = requiredPermissions.length === 0    ? true    : hasPermissions(requiredPermissions, permissionsMode);  if (!rolesOk || !permsOk) {    return <Navigate to={redirectTo} replace />;  }  return <Outlet />;}

Esse guard usa Outlet para proteger um grupo de rotas filhas.

3) Aplique o guard em grupos de rotas

Em vez de repetir guard em cada rota, agrupe por área. Exemplo: área administrativa exige role admin e permissão users:read para entrar.

const router = createBrowserRouter([  {    path: '/app',    element: <AuthenticatedLayout />,    children: [      {        element: (          <AuthorizationGuard            requiredRoles={['admin']}            requiredPermissions={['users:read']}            permissionsMode="all"          />        ),        children: [          { path: 'admin', element: <AdminHome /> },          { path: 'admin/users', element: <UsersList /> },        ],      },      {        element: (          <AuthorizationGuard requiredPermissions={['reports:view']} />        ),        children: [          { path: 'reports', element: <Reports /> },        ],      },    ],  },]);

Note que rotas diferentes podem exigir critérios diferentes. Isso facilita manutenção quando a aplicação cresce.

Proteção por componente: renderização condicional e componentes utilitários

Mesmo com rotas protegidas, você precisa controlar elementos dentro da página. Existem três padrões úteis: renderização condicional direta, componente <Can /> e wrappers de ação.

1) Renderização condicional direta

Para casos simples, use o hook de autorização e condicione o JSX.

function UsersToolbar() {  const { hasPermission } = useAuthorization();  return (    <div>      {hasPermission('users:create') && (        <button type="button">Novo usuário</button>      )}    </div>  );}

Isso é rápido, mas pode ficar repetitivo em telas grandes.

2) Componente <Can /> para reduzir repetição

Um componente utilitário centraliza a lógica e deixa o JSX mais limpo. Ele pode aceitar roles, permissões e modo.

type CanProps = {  roles?: string[];  permissions?: string[];  permissionsMode?: 'any' | 'all';  fallback?: React.ReactNode;  children: React.ReactNode;};function Can({  roles = [],  permissions = [],  permissionsMode = 'all',  fallback = null,  children,}: CanProps) {  const auth = useAuth();  const { hasPermissions } = useAuthorization();  const rolesOk = roles.length === 0    ? true    : roles.some(r => auth.user?.roles?.includes(r));  const permsOk = permissions.length === 0    ? true    : hasPermissions(permissions, permissionsMode);  if (!auth.isAuthenticated) return fallback;  if (!rolesOk || !permsOk) return fallback;  return <>{children}</>;}

Uso:

<Can permissions={['users:delete']}>  <button type="button">Excluir</button></Can>

Com fallback, você pode mostrar um botão desabilitado com tooltip, por exemplo, em vez de esconder completamente.

3) Protegendo ações (handlers) além da UI

Mesmo que você esconda um botão, ainda pode existir um caminho indireto para disparar uma ação (atalhos, componentes reutilizados, etc.). Uma prática útil é validar permissão também no handler e falhar de forma controlada.

function DeleteUserButton({ userId }: { userId: string }) {  const { hasPermission } = useAuthorization();  async function handleDelete() {    if (!hasPermission('users:delete')) {      // Exiba mensagem e não execute a chamada      throw new Error('Sem permissão para excluir usuários');    }    await api.delete(`/users/${userId}`);  }  return (    <button type="button" onClick={handleDelete}>Excluir</button>  );}

Isso não substitui a validação no back-end, mas evita comportamentos estranhos na UI e facilita testes.

Políticas condicionais: quando roles/permissões não bastam

Há cenários em que a permissão depende de contexto:

  • Usuário pode editar apenas seus próprios dados
  • Manager pode aprovar apenas solicitações do seu time
  • Não pode cancelar pedido após status “enviado”

Nesses casos, crie políticas como funções que recebem o usuário e o recurso.

type User = { id: string; roles: string[]; permissions: string[]; };type Profile = { id: string; ownerId: string; };function canEditProfile(currentUser: User, profile: Profile) {  const isAdmin = currentUser.roles.includes('admin');  const isOwner = profile.ownerId === currentUser.id;  const hasEditPerm = currentUser.permissions.includes('profiles:edit');  return hasEditPerm && (isAdmin || isOwner);}

Uso no componente:

function ProfilePage({ profile }: { profile: Profile }) {  const auth = useAuth();  const allowed = auth.user ? canEditProfile(auth.user, profile) : false;  return (    <div>      {allowed ? <button>Editar</button> : null}    </div>  );}

Esse padrão evita “gambiarras” de permissão e deixa explícito o motivo do acesso.

Mapeando requisitos de autorização junto das rotas

Para aplicações grandes, é comum querer uma “fonte” única de requisitos por rota, para:

  • Gerar menus dinamicamente
  • Evitar inconsistência entre menu e guard
  • Facilitar auditoria de acesso

Você pode criar uma estrutura de metadados de rotas (um array/objeto) com campos como path, label, requiredRoles, requiredPermissions. Exemplo:

type AppRoute = {  path: string;  label: string;  requiredRoles?: string[];  requiredPermissions?: string[];};const appRoutes: AppRoute[] = [  { path: '/app/admin', label: 'Admin', requiredRoles: ['admin'] },  { path: '/app/reports', label: 'Relatórios', requiredPermissions: ['reports:view'] },];

Com isso, você consegue filtrar itens de menu com o mesmo mecanismo do <Can />:

function AppMenu() {  const auth = useAuth();  const { hasPermissions } = useAuthorization();  const items = appRoutes.filter(r => {    if (!auth.isAuthenticated) return false;    const rolesOk = !r.requiredRoles?.length      ? true      : r.requiredRoles.some(role => auth.user?.roles?.includes(role));    const permsOk = !r.requiredPermissions?.length      ? true      : hasPermissions(r.requiredPermissions, 'all');    return rolesOk && permsOk;  });  return (    <ul>      {items.map(i => (        <li key={i.path}><a href={i.path}>{i.label}</a></li>      ))}    </ul>  );}

Se você usa Link do React Router, substitua o <a> por <Link to=...>.

Boas práticas e armadilhas comuns

Não confundir “não autenticado” com “não autorizado”

Se o usuário não está logado, redirecione para login. Se está logado e não tem permissão, redirecione para “não autorizado”. Misturar esses fluxos gera UX ruim e dificulta depuração.

Evite espalhar strings de permissão pela aplicação

Centralize permissões em constantes para evitar typos e facilitar refatoração.

export const PERMS = {  USERS_READ: 'users:read',  USERS_CREATE: 'users:create',  USERS_DELETE: 'users:delete',  REPORTS_VIEW: 'reports:view',} as const;

Uso:

<Can permissions={[PERMS.USERS_DELETE]}>...</Can>

Trate carregamento de dados do usuário antes de decidir

Se roles/permissões vêm de uma chamada assíncrona (ex.: endpoint /me), o guard deve considerar um estado de loading para não redirecionar indevidamente. Uma abordagem é: enquanto carrega, renderize um placeholder.

if (auth.isLoadingUser) {  return <p>Carregando...</p>;}

Sincronize menu, rotas e permissões

Um erro comum é: o menu mostra um item, mas a rota bloqueia; ou o menu esconde, mas a rota permite. Use a mesma fonte de requisitos (metadados) ou os mesmos helpers (hasPermissions, <Can />) para ambos.

Permissões no front-end são “sugestões” de UI

O front-end não deve assumir que “se tem permissão, a ação vai funcionar”. O back-end pode negar por contexto (ex.: recurso pertence a outro tenant). Portanto, trate erros 403/401 nas chamadas e reflita isso na UI.

Exemplo integrado: rota protegida + ações internas

Imagine uma página de usuários acessível para quem tem users:read. Dentro dela, criar exige users:create e excluir exige users:delete.

// Rotas{  element: <AuthorizationGuard requiredPermissions={['users:read']} />,  children: [    { path: '/app/users', element: <UsersPage /> },  ],}// Páginafunction UsersPage() {  return (    <div>      <h3>Usuários</h3>      <Can permissions={['users:create']}>        <button type="button">Novo usuário</button>      </Can>      <UsersTable />    </div>  );}function UsersTable() {  const users = [{ id: '1', name: 'Ana' }];  return (    <ul>      {users.map(u => (        <li key={u.id}>          {u.name}          <Can permissions={['users:delete']} fallback={null}>            <button type="button">Excluir</button>          </Can>        </li>      ))}    </ul>  );}

Esse arranjo cria uma experiência coerente: quem não pode ver a lista nem entra na rota; quem pode ver, mas não pode criar/excluir, ainda consegue consultar.

Agora responda o exercício sobre o conteúdo:

Em uma SPA, qual combinação descreve corretamente o papel da autorização por rota e por componente?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A autorização por rota evita acesso a rotas indevidas (inclusive por URL). A autorização por componente limita botões e ações dentro de páginas permitidas. Em ambos os casos, a segurança real deve ser aplicada no back-end.

Próximo capitúlo

Redirecionamentos pós-login e preservação da rota pretendida (returnUrl)

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.