Implementação de Login Único com MetaMask e Web3.0 em Aplicações Vue.js e Tornado

A computação descentralizada, com raízes que remontam a conceitos como a eliminação de pontos centrais de falha, ganhou força com a ascensão da internet. Hoje, a ideia de identidade descentralizada está se tornando cada vez mais relevante. A atividade online de um usuário, quando registrada em blockchain, pode revelar seus interesses e construir uma identidade digital única e verificável.

Em contraste com os tradicionais fluxos de registro por e-mail e senha, ou mesmo os convenientes logins sociais (Google, GitHub), que expõem dados a terceiros e permitem a criação de perfis comportamentais, o Web3.0 oferece uma alternativa focada na privacidade: o MetaMask.

MetaMask

MetaMask é uma carteira de criptomoedas que facilita a interação com a blockchain Ethereum. Através de extensões de navgeador e aplicativos móveis, o MetaMask permite que os usuários acessem suas carteiras e interajam com aplicações descentralizadas (dApps). Para utilizá-lo, é necessário primeiro possuir uma carteira MetaMask, que pode ser configurada através da loja de extensões do navegador (por exemplo, para Chrome).

Após a instalação, crie uma conta e guarde com segurança sua senha, endereço da carteira e frase de recuperação (seed phrase).

Fluxo de Login com Carteira

O processo de login com MetaMask difere significativamente dos logins de terceiros tradicionais. Em vez de redirecionamentos e autorizações de plataformas externas, o login com carteira envolve uma assinatura digital realizada no lado do cliente. O processo geral é:

  1. No frontend, o usuário inicia o processo de login.
  2. A aplicação solicita permissão para acessar a conta MetaMask.
  3. Uma mensagem criptografada é criada, contendo informações como o domínio do site e um timestamp.
  4. O MetaMask é usado para assinar essa mensagem com a chave privada do usuário.
  5. O endereço público da carteira e a assinatura digital são enviados ao backend.
  6. O backend verifica a assinatura usando a chave pública correspondente e a mensagem original.
  7. Se a verificação for bem-sucedida, uma sessão é estabelecida para o usuário, geralmente por meio de um token JWT.

Diagrama de Fluxo de Login com Carteira

Assinatura no Frontend (Vue.js e Ethers.js)

Para implementar a assinatura no frontend, utilizaremos a biblioteca ethers.js. Abaixo está um exemplo de como integrar e realizar a assinatura usando Vue.js e Axios para comunicação com o backend.

Primeiro, inclua as bbiliotecas necessárias no seu HTML:


<script src="path/to/ethers-v4.min.js"></script>
<script src="path/to/axios.js"></script>
<script src="path/to/vue.js"></script>

Em seguida, no componente Vue, defina a lógica para a ativação do login:


const App = {
    data() {
        return {
            email: "",
            password: "",
            provider: null,
            accountAddress: "",
            signer: null
        };
    },
    methods: {
        async connectWallet() {
            if (typeof ethereum === 'undefined') {
                alert('MetaMask não está instalado. Por favor, instale a extensão.');
                return;
            }
            try {
                // Solicita acesso à conta
                await ethereum.request({ method: 'eth_requestAccounts' });
                this.provider = new ethers.providers.Web3Provider(window.ethereum);
                const network = await this.provider.getNetwork();

                // Verifica se está na rede principal Ethereum (Chain ID 1)
                if (network.chainId !== 1) {
                    console.log("Por favor, mude para a rede principal Ethereum.");
                    // Opcional: pode adicionar lógica para mudar de rede
                    return;
                }

                const accounts = await this.provider.listAccounts();
                this.accountAddress = accounts[0];
                this.signer = this.provider.getSigner();

                // Cria a mensagem a ser assinada com um timestamp para evitar replay attacks
                const now = Math.floor(Date.now() / 1000);
                // O timestamp é arredondado para evitar pequenas variações que invalidariam a assinatura
                const sessionTimestamp = now - (now % 600); // Agrupa por blocos de 10 minutos
                const message = `Autenticar em ${document.domain} às ${sessionTimestamp}`;

                // Assina a mensagem
                const signature = await this.signer.signMessage(message);
                console.log("Endereço:", this.accountAddress);
                console.log("Assinatura:", signature);

                // Envia o endereço e a assinatura para o backend para verificação
                this.verifySignature(this.accountAddress, signature);

            } catch (error) {
                console.error("Erro ao conectar a carteira:", error);
                alert("Falha ao conectar ou assinar com a MetaMask.");
            }
        },

        async verifySignature(address, signature) {
            try {
                const response = await axios.post('/api/verify-wallet', {
                    public_address: address,
                    signature: signature
                });

                if (response.data.errcode === 0) {
                    alert(`Bem-vindo: ${response.data.public_address}`);
                    localStorage.setItem("authToken", response.data.token);
                    localStorage.setItem("userAddress", response.data.public_address);
                    window.location.href = "/"; // Redireciona após login bem-sucedido
                } else {
                    alert("Falha na verificação da assinatura.");
                }
            } catch (error) {
                console.error("Erro na verificação da assinatura:", error);
                alert("Erro ao comunicar com o servidor para verificação.");
            }
        },

        async traditionalLogin() {
            if (!this.email || !this.password) {
                alert("Por favor, preencha o e-mail e a senha.");
                return;
            }
            try {
                const response = await axios.get('/api/login', {
                    params: {
                        email: this.email,
                        password: this.password
                    }
                });
                if (response.data.errcode === 0) {
                    alert(response.data.msg);
                    localStorage.setItem("authToken", response.data.token);
                    localStorage.setItem("userEmail", response.data.email);
                    window.location.href = "/";
                } else {
                    alert(response.data.msg);
                }
            } catch (error) {
                console.error("Erro no login tradicional:", error);
                alert("Ocorreu um erro durante o login.");
            }
        }
    }
};

// Instanciando a aplicação Vue
const app = Vue.createApp(App);
// Assumindo que myaxios e axios estão globalmente disponíveis ou configurados
// app.config.globalProperties.myaxios = myaxios;
// app.config.globalProperties.axios = axios;
app.mount("#app");


Verificação no Backend (Tornado)

No backend, utilizamos o framework Tornado e a biblioteca web3.py para verificar a assinatura. Isso garante que a assinatura foi realmente gerada pela chave privada associada ao endereço público apresentado.

Instale as dependências necessárias:


pip install tornado==6.1
pip install web3==5.29.1
pip install eth-account

Crie um manipulador (handler) assíncrono para lidar com a verificação:


import tornado.web
from web3 import Web3
from eth_account.messages import encode_defunct
import time
from tornado.escape import json_decode

# Assumindo que BaseHandler e MyJwt são definidos em outros módulos
# from .base import BaseHandler
# from utils.jwt_handler import MyJwt
# from models.user_model import User # Exemplo de modelo de usuário

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        # Implementação de autenticação baseada em token JWT, se necessário
        pass

    def get_json_body(self):
        try:
            return json_decode(self.request.body)
        except:
            return None

class CheckWalletSignatureHandler(BaseHandler):
    async def post(self):
        data = self.get_json_body()
        public_address = data.get("public_address")
        signature = data.get("signature")

        if not public_address or not signature:
            self.set_status(400)
            self.write({"msg": "Endereço público e assinatura são obrigatórios.", "errcode": 1})
            return

        # Extrai o domínio da requisição
        domain = self.request.host
        if ":" in domain:
            domain = domain.split(":")[0]

        now = int(time.time())
        # Recria a mensagem original usada no frontend
        session_timestamp = now - (now % 600)
        original_message = f"Autenticar em {domain} às {session_timestamp}"

        try:
            # Codifica a mensagem no formato esperado pela função de recuperação
            message_hash = encode_defunct(text=original_message)
            # Recupera o endereço que assinou a mensagem
            signer_address = Web3().eth.account.recover_message(message_hash, signature=signature)

            # Compara o endereço recuperado com o endereço fornecido
            if signer_address.lower() == public_address.lower():
                # Lógica para encontrar ou criar o usuário no banco de dados
                user_email = public_address # Usando o endereço como identificador/email
                user = None
                try:
                    # Exemplo: buscar usuário no banco de dados
                    # user = await self.application.objects.get(User, email=user_email)
                    pass # Substituir pela lógica real de busca/criação de usuário
                except Exception: # Erro ao buscar, significa que o usuário não existe
                    # Exemplo: criar novo usuário
                    # from django.contrib.auth.hashers import make_password
                    # user = await self.application.objects.create(User, email=user_email, password=make_password("generated_password_for_wallet_users"), role=1)
                    pass # Substituir pela lógica real de criação

                # Gera um token JWT para o usuário
                myjwt = MyJwt() # Instancie sua classe de manipulação JWT
                token = myjwt.encode({"id": user.id if user else "new_user_id"}) # Use o ID real do usuário

                self.write({"msg": "Autenticação bem-sucedida", "errcode": 0, "public_address": public_address, "token": token})
            else:
                self.write({"msg": "Falha na verificação da assinatura.", "errcode": 1})

        except Exception as e:
            print(f"Erro na verificação da assinatura: {e}")
            self.set_status(500)
            self.write({"msg": "Erro interno do servidor.", "errcode": 1})

# Configuração de rotas no Tornado (exemplo)
# url_handlers = [
#     (r"/api/verify-wallet", CheckWalletSignatureHandler),
#     # ... outras rotas
# ]

Após a verificação bem-sucedida da assinatura no backend, um token de autenticação é gerado e enviado de volta ao frontend. Este token pode ser armazenado localmente (por exemplo, no localStorage) e usado para autenticar requisições futuras à API.

Considerações Finais

A integração de identidades Web3.0 como MetaMask representa um avanço significativo em termos de privacidade e controle do usuário sobre seus dados. Embora a adoção de novas tecnologias possa enfrentar resistência inicial, a tendência de descentralização e o foco na privacidade do usuário são forças impulsionadoras que moldarão o futuro das aplicações online. O código-fonte completo deste exemplo está disponível em GitHub.

Tags: web3.0 metamask Vue.js ethers.js Tornado

Publicado em 7-9 16:26