Reagir autenticação de token


Problemm


A autorização é um dos primeiros problemas que os desenvolvedores enfrentam ao iniciar um novo projeto. E um dos tipos mais comuns de autorização (pela minha experiência) é a autorização baseada em token (geralmente usando JWT).


Na minha perspectiva, este artigo se parece com "o que eu queria ler duas semanas atrás". Meu objetivo era escrever código minimalista e reutilizável com uma interface limpa e direta. Eu tinha os seguintes requisitos para minha implementação do gerenciamento de autenticação:


  • Os tokens devem ser armazenados no armazenamento local
  • Os tokens devem ser restaurados na recarga da página
  • O token de acesso deve ser passado nas solicitações de rede
  • Após a expiração, o token de acesso deve ser atualizado pelo token de atualização, se o último for apresentado
  • Os componentes de reação devem ter acesso às informações de autenticação para renderizar a interface do usuário apropriada
  • A solução deve ser feita com React puro (sem Redux, thunk, etc.)

Para mim, uma das perguntas mais desafiadoras foi:


  • Como manter sincronizado o estado dos componentes do React e os dados de armazenamento local?
  • Como obter o token dentro da busca sem passar por toda a árvore de elementos (especialmente se queremos usar essa busca em ações de thunk posteriormente, por exemplo)

Mas vamos resolver os problemas passo a passo. Primeiro, criaremos um token provider para armazenar tokens e oferecer a possibilidade de ouvir as alterações. Depois disso, criaremos um auth provider , na verdade envolvente, em torno do token provider para criar ganchos para componentes React, buscar esteróides e alguns métodos adicionais. E no final, veremos como usar essa solução no projeto.


Eu só quero npm install ... e começar a produção


Eu já reuni o pacote que contém todos os descritos abaixo (e um pouco mais). Você só precisa instalá-lo pelo comando:


 npm install react-token-auth 

E siga os exemplos no repositório GitHub react-token-auth .


Solução


Antes de resolver o problema, assumirei que temos um back-end que retorna um objeto com acesso e atualização de tokens. Cada token possui um formato JWT . Esse objeto pode se parecer com:


 { "accessToken": "...", "refreshToken": "..." } 

Na verdade, a estrutura do objeto tokens não é crítica para nós. No caso mais simples, pode ser uma sequência com um token de acesso infinito. Mas queremos ver como gerenciar uma situação quando temos dois tokens, um deles pode expirar e o segundo pode ser usado para atualizar o primeiro.


Jwt


Se você não souber qual é o token JWT, a melhor opção é acessar o jwt.io e ver como ele funciona. Agora é importante que o token JWT contenha informações codificadas (no formato Base64 ) sobre o usuário que permita autenticá-lo no servidor.


Normalmente, o token JWT contém 3 partes divididas por pontos e se parece com:


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.yOZC0rjfSopcpJ-d3BWE8-BkoLR_SCqPdJpq8Wn-1Mc


Se decodificarmos a parte do meio ( eyJu...Mn0 ) desse token, obteremos o próximo JSON:


 { "name": "John Doe", "iat": 1516239022, "exp": 1516239022 } 

Com essas informações, poderemos obter a data de validade do token.


Fornecedor de tokens


Como mencionei antes, nossa primeira etapa é criar o provedor de token. O provedor de token trabalhará diretamente com o armazenamento local e todas as alterações de token que faremos através dele. Isso nos permitirá ouvir alterações de qualquer lugar e notificar imediatamente os ouvintes sobre alterações (mas um pouco mais tarde). A interface do provedor terá os seguintes métodos:


  • getToken() para obter o token atual (ele será usado na busca)
  • setToken() para definir o token após o login, logout ou registro
  • isLoggedIn() para verificar se o usuário está logado
  • subscribe() para fornecer ao provedor uma função que deve ser chamada após qualquer alteração de token
  • unsubscribe() para remover o assinante

A função createTokenProvider() criará uma instância do provedor de token com a interface descrita:


 const createTokenProvider = () => { /* Implementation */ return { getToken, isLoggedIn, setToken, subscribe, unsubscribe, }; }; 

Todo o próximo código deve estar dentro da função createTokenProvider.


Vamos começar criando uma variável para armazenar tokens e restaurar os dados do armazenamento local (para garantir que a sessão não seja perdida após o recarregamento da página):


 let _token: { accessToken: string, refreshToken: string } = JSON.parse(localStorage.getItem('REACT_TOKEN_AUTH') || '') || null; 

Agora, precisamos criar algumas funções adicionais para trabalhar com tokens JWT. No momento atual, o token JWT parece uma sequência mágica, mas não é importante analisá-lo e tentar extrair a data de validade. A função getExpirationDate() um token JWT como parâmetro e retornará o registro de data e hora da expiração em caso de sucesso (ou null em caso de falha):


 const getExpirationDate = (jwtToken?: string): number | null => { if (!jwtToken) { return null; } const jwt = JSON.parse(atob(jwtToken.split('.')[1])); // multiply by 1000 to convert seconds into milliseconds return jwt && jwt.exp && jwt.exp * 1000 || null; }; 

E mais uma função util isExpired() para verificar é o carimbo de data / hora expirado. Esta função retorna true se o carimbo de data / hora de vencimento apresentado e for menor que Date.now() .


 const isExpired = (exp?: number) => { if (!exp) { return false; } return Date.now() > exp; }; 

Hora de criar a primeira função da interface do provedor de token. A função getToken() deve retornar o token e atualizá-lo, se necessário. Essa função deve ser async pois pode fazer uma solicitação de rede para atualizar o token.


Usando funções criadas anteriormente, podemos verificar se os tokens de acesso expiraram ou não ( isExpired(getExpirationDate(_token.accessToken)) ). E, no primeiro caso, para solicitar uma atualização do token. Depois disso, podemos salvar os tokens (com a função ainda não implementada setToken() ). E, finalmente, podemos retornar o token de acesso:


 const getToken = async () => { if (!_token) { return null; } if (isExpired(getExpirationDate(_token.accessToken))) { const updatedToken = await fetch('/update-token', { method: 'POST', body: _token.refreshToken }) .then(r => r.json()); setToken(updatedToken); } return _token && _token.accessToken; }; 

A função isLoggedIn() será simples: retornará true se _tokens não for null e não verificará a expiração do token de acesso (nesse caso, não saberemos sobre o token de acesso de expiração até que ocorram falhas ao obter o token, mas geralmente é suficiente , e vamos manter a função isLoggedIn synchronous):


 const isLoggedIn = () => { return !!_token; }; 

Acho que é um bom momento para criar funcionalidades para gerenciar observadores. Implementaremos algo semelhante ao padrão Observer e, antes de tudo, criaremos uma matriz para armazenar todos os nossos observadores. Vamos esperar que cada elemento desse array seja a função que deveríamos chamar após cada alteração de tokens:


 let observers: Array<(isLogged: boolean) => void> = []; 

Agora podemos criar métodos subscribe() e unsubscribe() . O primeiro adicionará um novo observador ao array criado um pouco mais cedo, o segundo removerá o observador da lista.


 const subscribe = (observer: (isLogged: boolean) => void) => { observers.push(observer); }; const unsubscribe = (observer: (isLogged: boolean) => void) => { observers = observers.filter(_observer => _observer !== observer); }; 

Você já pode ver na interface das funções subscribe() e unsubscribe() que enviaremos aos observadores apenas o fato de o usuário estar logado. Mas, em geral, você pode enviar tudo o que deseja (todo o token, tempo de validade, etc ...). Mas, para nossos propósitos, será suficiente enviar uma bandeira booleana.


Vamos criar uma pequena função util notify() que receberá este sinalizador e enviará a todos os observadores:


 const notify = () => { const isLogged = isLoggedIn(); observers.forEach(observer => observer(isLogged)); }; 

E por último, mas não menos importante, a função que precisamos implementar é a setToken() . O objetivo desta função é salvar tokens no armazenamento local (ou limpar o armazenamento local se o token estiver vazio) e notificar os observadores sobre alterações. Então, eu vejo o objetivo, vou para o objetivo.


 const setToken = (token: typeof _token) => { if (token) { localStorage.setItem('REACT_TOKEN_AUTH', JSON.stringify(token)); } else { localStorage.removeItem('REACT_TOKEN_AUTH'); } _token = token; notify(); }; 

Certifique-se de que, se você chegou a esse ponto do artigo e achou útil, você já me deixou mais feliz. Aqui terminamos com o provedor de token. Você pode olhar para o seu código, brincar com ele e verificar se ele funciona. Na próxima parte, criaremos mais funcionalidades abstratas que já serão úteis em qualquer aplicativo React.


Provedor de autenticação


Vamos criar uma nova classe de objetos que chamaremos de provedor de autenticação. A interface conterá 4 métodos: hook useAuth() para obter um novo status do componente React, authFetch() para fazer solicitações à rede com os métodos token e login() , logout() atuais que farão proxy das chamadas para o método setToken() do provedor de tokens (nesse caso, teremos apenas um ponto de entrada para toda a funcionalidade criada e o restante do código não precisará saber sobre a existência do provedor de tokens). Como antes, começaremos com o criador da função:


 export const createAuthProvider = () => { /* Implementation */ return { useAuth, authFetch, login, logout } }; 

Primeiro de tudo, se queremos usar um provedor de token, precisamos criar uma instância dele:


 const tokenProvider = createTokenProvider(); 

Os métodos login() e logout() simplesmente passam o token para o provedor de token. Separei esses métodos apenas por significado explícito (na verdade, a passagem de token vazio / nulo remove dados do armazenamento local):


 const login: typeof tokenProvider.setToken = (newTokens) => { tokenProvider.setToken(newTokens); }; const logout = () => { tokenProvider.setToken(null); }; 

O próximo passo é a função de busca. De acordo com minha ideia, essa função deve ter exatamente a mesma interface que a busca original e retornar o mesmo formato, mas deve injetar o token de acesso a cada solicitação.


A função de busca deve ter dois argumentos: request info (geralmente URL) e request init (um objeto com método, body. Headers e assim por diante); e retorna promessa para a resposta:


 const authFetch = async (input: RequestInfo, init?: RequestInit): Promise<Response> => { const token = await tokenProvider.getToken(); init = init || {}; init.headers = { ...init.headers, Authorization: `Bearer ${token}`, }; return fetch(input, init); }; 

Dentro da função, fizemos duas coisas: pegamos um token do provedor de token pela instrução await tokenProvider.getToken(); ( getToken já contém a lógica de atualizar o token após a expiração) e injetar esse token no cabeçalho da Authorization pela linha Authorization: 'Bearer ${token}' . Depois disso, simplesmente retornamos a busca com argumentos atualizados.


Portanto, já podemos usar o provedor de autenticação para salvar tokens e usá-los da busca. O último problema é que não podemos reagir às alterações de token de nossos componentes. Hora de resolvê-lo.


Como eu disse antes, criaremos um gancho useAuth() que fornecerá informações ao componente, seja o usuário logado ou não. Para poder fazer isso, usaremos hook useState() para manter essas informações. É útil porque quaisquer alterações nesse estado causarão a renderização novamente dos componentes que usam esse gancho.


E já preparamos tudo para poder ouvir as alterações no armazenamento local. Uma maneira comum de ouvir quaisquer alterações no sistema com ganchos é usando o gancho useEffect() . Este gancho usa dois argumentos: função e lista de dependências. A função será acionada após a primeira chamada de useEffect e, em seguida, reiniciada após qualquer alteração na lista de dependências. Nesta função, podemos começar a ouvir as alterações no armazenamento local. Mas o que é importante, podemos retornar dessa função ... nova função e, essa nova função será acionada antes de reiniciar a primeira ou após a desmontagem do componente. Na nova função, podemos parar de ouvir as alterações e as garantias do React, de que essa função será acionada (pelo menos se nenhuma exceção acontecer durante esse processo). Parece um pouco complicado, mas basta olhar para o código:


 const useAuth = () => { const [isLogged, setIsLogged] = useState(tokenProvider.isLoggedIn()); useEffect(() => { const listener = (newIsLogged: boolean) => { setIsLogged(newIsLogged); }; tokenProvider.subscribe(listener); return () => { tokenProvider.unsubscribe(listener); }; }, []); return [isLogged] as [typeof isLogged]; }; 

E isso é tudo. Acabamos de criar armazenamento de autenticação de token compacto e reutilizável com API clara. Na próxima parte, veremos alguns exemplos de uso.


Uso


Para começar a usar o que implementamos acima, precisamos criar uma instância do provedor de autenticação. Isso nos dará acesso às funções useAuth() , authFetch() , login() , logout() relacionadas ao mesmo token no armazenamento local (em geral, nada impede que você crie instâncias diferentes do provedor de autenticação para tokens diferentes, mas você precisará parametrizar a chave usada para armazenar dados no armazenamento local):


 export const {useAuth, authFetch, login, logout} = createAuthProvider(); 

Formulário de login


Agora podemos começar a usar as funções que obtivemos. Vamos começar com o componente do formulário de login. Este componente deve fornecer entradas para as credenciais do usuário e salvá-lo no estado interno. Ao enviar, precisamos enviar uma solicitação com as credenciais para obter tokens e aqui podemos usar a função login() para armazenar os tokens recebidos:


 const LoginComponent = () => { const [credentials, setCredentials] = useState({ name: '', password: '' }); const onChange = ({target: {name, value}}: ChangeEvent<HTMLInputElement>) => { setCredentials({...credentials, [name]: value}) }; const onSubmit = (event?: React.FormEvent) => { if (event) { event.preventDefault(); } fetch('/login', { method: 'POST', body: JSON.stringify(credentials) }) .then(r => r.json()) .then(token => login(token)) }; return <form onSubmit={onSubmit}> <input name="name" value={credentials.name} onChange={onChange}/> <input name="password" value={credentials.password} onChange={onChange}/> </form> }; 

E é tudo, é tudo o que precisamos para armazenar o token. Depois disso, quando um token é recebido, não precisamos aplicar esforços extras para buscá-lo ou em componentes, porque ele já está implementado dentro do provedor de autenticação.


O formulário de registro é semelhante, existem apenas diferenças no número e nos nomes dos campos de entrada, portanto vou omitir aqui.


Roteador


Além disso, podemos implementar o roteamento usando o provedor de autenticação. Vamos supor que temos dois pacotes de rotas: um para o usuário registrado e outro para não registrado. Para dividi-los, precisamos verificar se temos um token no armazenamento local ou não, e aqui podemos usar hook useAuth() :


 export const Router = () => { const [logged] = useAuth(); return <BrowserRouter> <Switch> {!logged && <> <Route path="/register" component={Register}/> <Route path="/login" component={Login}/> <Redirect to="/login"/> </>} {logged && <> <Route path="/dashboard" component={Dashboard} exact/> <Redirect to="/dashboard"/> </>} </Switch> </BrowserRouter>; }; 

E o bom de ser renderizado novamente após qualquer alteração no armazenamento local, devido ao useAuth ter uma assinatura para essas alterações.


Buscar solicitações


E então podemos proteger os dados pelo token usando authFetch . Ele tem a mesma interface que a busca, portanto, se você já usa a busca no código, pode simplesmente substituí-lo por authFetch :


 const Dashboard = () => { const [posts, setPosts] = useState([]); useEffect(() => { authFetch('/posts') .then(r => r.json()) .then(_posts => setPosts(_posts)) }, []); return <div> {posts.map(post => <div key={post.id}> {post.message} </div>)} </div> }; 

Sumário


Nós conseguimos. Foi uma jornada interessante, mas também tem o fim (talvez até feliz).


Começamos com o entendimento de problemas com o armazenamento de tokens de autorização. Em seguida, implementamos uma solução e finalmente analisamos os exemplos de como ela pode ser usada no aplicativo React.


Como eu disse antes, você pode encontrar minha implementação no GitHub na biblioteca. Ele resolve um problema um pouco mais genérico e não faz suposições sobre a estrutura do objeto com tokens ou sobre como atualizar o token, portanto, você precisará fornecer alguns argumentos extras. Mas a idéia da solução é a mesma e o repositório também contém instruções sobre como usá-la.


Aqui posso dizer obrigado pela leitura do artigo e espero que tenha sido útil para você.

Source: https://habr.com/ru/post/pt485764/


All Articles