Implementação de Autenticação de Terceiros com QQ em Arquiteturas Decopladas

A integração de sistemas de login de terceiros é uma prática essencial para melhorar a experiência do usuário e facilitar o processo de registro em aplicações modernas. Ao adotar o protocolo OAuth 2.0, como no caso do provedor QQ, o sistema não armazena senhas externas, mas sim um identificador único (OpenID) e um token de acesso (AccessToken) que garantem a identidade do usuário de forma segura.

1. Fundamentos do Fluxo OAuth 2.0

O processo baseia-se na confiança entre o provedor de identidade (QQ), o usuário e a sua aplicação. O fluxo padrão consiste em redirecionar o usuário para o portal do QQ, onde ele autoriza o acesso. Após a autorização, o provedor devolve um código temporário para a sua URL de retorno (callback), que o backend utiliza para trocar por credenciais de acesso definitivas.

Antes de iniciar o desenvolvimento técnico, é obrigatório possuir um domínio registrado e validado junto ao QQ Connect, além de completar a verificação de identidade de desenvolvedor na plataforma.

2. Implementação no Frontend

Em aplicações onde o frontend e o backend estão separados, uma técnica comum é abrir uma janela pop-up centralizada para gerenciar a interação com o provedor de autenticação, evitando que o usuário perca o estado atual da aplicação principal.

/**
 * Abre uma janela centralizada para o fluxo de autenticação externa
 */
function dispararLoginExterno(enderecoUrl, larguraJanela = 650, alturaJanela = 450) {
    const eixoX = (window.screen.width - larguraJanela) / 2;
    const eixoY = (window.screen.height - alturaJanela) / 2;
    
    const configuracoes = `toolbar=no, location=no, directories=no, status=no, menubar=no, 
                          scrollbars=yes, resizable=no, copyhistory=no, 
                          width=${larguraJanela}, height=${alturaJanela}, 
                          top=${eioY}, left=${eixoX}`;
    
    const janelaPopup = window.open(enderecoUrl, "AutenticacaoQQ", configuracoes);
    
    if (window.focus && janelaPopup) {
        janelaPopup.focus();
    }
}

3. Lógica do Backend com Spring Boot

O backend desempenha dois papéis: gerar a URL de autorização inicial e processar o retorno do provedor para validar o usuário.

@RestController
@RequestMapping("/auth/provedor/qq")
public class ControleAutenticacaoQQ {
      
      @Autowired
      private ConfiguracaoOauth configuracoes;
      
      private static final Logger logTecnico = LoggerFactory.getLogger(ControleAutenticacaoQQ.class);
      
      /**
       * Gera o endpoint de redirecionamento para o portal do QQ
       */
      @GetMapping("/url-autorizacao")
      public String obterUrlRedirecionamento() {
            String stateAleatorio = UUID.randomUUID().toString().replace("-", "");
            
            StringBuilder construtorUrl = new StringBuilder(configuracoes.getUriAutorizacao());
            construtorUrl.append("?client_id=").append(configuracoes.getAppId())
                        .append("&state=").append(stateAleatorio)
                        .append("&redirect_uri=").append(configuracoes.getCallbackUrl())
                        .append("&response_type=code");
            
            return construtorUrl.toString();
      }
      
      /**
       * Processa o código recebido após o consentimento do usuário
       */
      @GetMapping("/callback")
      public String gerenciarRetorno(@RequestParam("code") String codigoAutorizacao) {
            Map<String, String> parametrosToken = new HashMap<>();
            parametrosToken.put("code", codigoAutorizacao);
            parametrosToken.put("grant_type", "authorization_code");
            parametrosToken.put("redirect_uri", configuracoes.getCallbackUrl());
            parametrosToken.put("client_id", configuracoes.getAppId());
            parametrosToken.put("client_secret", configuracoes.getAppKey());

            // 1. Troca o código pelo Access Token
            String respostaToken = HttpCliente.executarGet(configuracoes.getUriToken(), parametrosToken);
            Map<String, String> mapaResposta = formatarResposta(respostaToken);
            String tokenAcesso = mapaResposta.get("access_token");

            // 2. Obtém o identificador único (OpenID)
            String respostaOpenId = HttpCliente.executarGet(configuracoes.getUriOpenId() + "?access_token=" + tokenAcesso);
            String jsonLimpo = respostaOpenId.substring(respostaOpenId.indexOf("{"), respostaOpenId.indexOf("}") + 1);
            DadosIdentidadeQQ identidade = new Gson().fromJson(jsonLimpo, DadosIdentidadeQQ.class);
            
            // 3. Recupera os dados de perfil do usuário
            Map<String, String> parametrosPerfil = new HashMap<>();
            parametrosPerfil.put("access_token", tokenAcesso);
            parametrosPerfil.put("openid", identidade.getOpenid());
            parametrosPerfil.put("oauth_consumer_key", configuracoes.getAppId());
            
            String dadosUsuarioJson = HttpCliente.executarGet(configuracoes.getUriUserInfo(), parametrosPerfil);
            PerfilUsuarioQQ perfil = new Gson().fromJson(dadosUsuarioJson, PerfilUsuarioQQ.class);
            
            return integrarComSistemaSeguranca(perfil, identidade.getOpenid());
      }

      private Map<String, String> formatarResposta(String raw) {
            Map<String, String> resultado = new HashMap<>();
            String[] partes = raw.split("&");
            for (String par : partes) {
                  String[] kv = par.split("=");
                  if (kv.length > 1) resultado.put(kv[0], kv[1]);
            }
            return resultado;
      }

      private String integrarComSistemaSeguranca(PerfilUsuarioQQ perfil, String uidExterno) {
            try {
                  Subject sujeito = SecurityUtils.getSubject();
                  // Utiliza o apelido do QQ para login ou o OpenID como credencial única
                  UsernamePasswordToken credencial = new UsernamePasswordToken(perfil.getNickname(), uidExterno);
                  sujeito.login(credencial);
                  return "/dashboard.html";
            } catch (Exception erro) {
                  logTecnico.error("Falha na autenticação via QQ: {}", erro.getMessage());
                  return "/erro-login.html";
            }
      }
}

4. Armazenamento e Sincronização

Ao receber o openID, o sistema deve verificar se este identificador já está vinculado a uma conta local no banco de dados. Caso seja um novo usuário, recomenda-se criar um registro persistindo o openID e as informações básicas de perfil (como nome e foto) para sessões futuras. Em arquiteturas desacopladas, após o sucesso do login, o backend geralmente emite um token JWT ou gerencia a sessão via cookies seguros para manter o estado de autenticação no cliente.

Tags: OAuth2 spring-boot java rest-api authentication

Publicado em 6-13 08:40 por Thomas