Implementación de Autenticación OAuth 2.0 con QQ en Spring Boot

La integración de sistemas de autenticación de terceros permite a los usuarios acceder a aplicaciones utilizando sus cuentas existentes, mejorando la experiencia de usuario y facilitando la adquisición de clientes. Este proceso se basa fundamentalmente en el protocolo OAuth 2.0. Al implemenatr el inicio de sesión con QQ, el flujo estándar garantiza la seguridad mediante el intercambio de un código de autorización por un accessToken, el cual posteriormente se utiliza para obtener el identificador único del usuario (openID) y su información de perfil.

Requisitos Previos y Configuración Inicial

Para interactuar con la API de QQ, es indispensable registrar una aplicación en la plataforma QQ Connect. Esto requiere una cuenta de desarrollador verificada y un nombre de dominio válido y reservado. Una vez aprobada la aplicación, se obtendrán el App ID y el App Key necesarios para las solicitudes de autenticación.

Interfaz de Usuario con Thymeleaf

Se requieren dos vistas básicas: una para iniciar el flujo de autenticación y otra para mostrar los datos del usuario una vez completado el proceso.

login.html


<html lang="es" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Acceso de Usuarios</title>
</head>
<body>
    <h2>Portal de Acceso</h2>
    <form action="/auth/qq/initiate" method="get">
        <button type="submit">Iniciar sesión con QQ</button>
    </form>
</body>
</html>

dashboard.html


<html lang="es" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Panel de Control</title>
</head>
<body>
    <h2>Autenticación Exitosa</h2>
    <div>
        <p th:text="'Identificador (OpenID): ' + ${userData.openId}"></p>
        <p th:text="'Nombre de usuario: ' + ${userData.profile.nickname}"></p>
        <p>Avatar:</p>
        <img th:src="${userData.profile.figureurl_qq_1}" alt="Avatar del usuario" width="50" height="50"/>
    </div>
</body>
</html>

Configuración del Backend

Se deben incluir las dependencias necesarias para el manejo de peticiones HTTP, procesamiento de JSON y la integración con el motor de plentillas.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Las credenciales y endpoints se externalizan en el archivo de configuración:

server:
  port: 8080

app:
  integration:
    qq:
      client-id: TU_APP_ID_AQUI
      client-secret: TU_APP_SECRET_AQUI
      redirect-uri: http://tu-dominio.com/auth/qq/callback
      endpoints:
        authorize: https://graph.qq.com/oauth2.0/authorize
        token: https://graph.qq.com/oauth2.0/token
        open-id: https://graph.qq.com/oauth2.0/me
        user-info: https://graph.qq.com/user/get_user_info

spring:
  thymeleaf:
    cache: false

Lógica de Controlador y Flujo OAuth 2.0

El controlador gestiona la redirección inicial, la recepción del código de autorización, el intercambio por el token de acceso y la obtención final del perfil. Se utiliza RestTemplate para las llamadas HTTP y se implementa un manejo robusto para las respuestas específicas de la API de QQ (como el formato JSONP para el OpenID).

package com.example.auth.controller;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
@Controller
@RequestMapping("/auth/qq")
@RequiredArgsConstructor
public class QqOAuthController {

    private final RestTemplate restTemplate = new RestTemplate();
    private final Gson gson = new Gson();

    @Value("${app.integration.qq.client-id}")
    private String clientId;

    @Value("${app.integration.qq.client-secret}")
    private String clientSecret;

    @Value("${app.integration.qq.redirect-uri}")
    private String redirectUri;

    @Value("${app.integration.qq.endpoints.authorize}")
    private String authorizeUrl;

    @Value("${app.integration.qq.endpoints.token}")
    private String tokenUrl;

    @Value("${app.integration.qq.endpoints.open-id}")
    private String openIdUrl;

    @Value("${app.integration.qq.endpoints.user-info}")
    private String userInfoUrl;

    @GetMapping("/initiate")
    public String initiateOAuthFlow() {
        String state = UUID.randomUUID().toString();
        String authRedirect = UriComponentsBuilder.fromHttpUrl(authorizeUrl)
                .queryParam("response_type", "code")
                .queryParam("client_id", clientId)
                .queryParam("redirect_uri", redirectUri)
                .queryParam("state", state)
                .build().toUriString();
        
        return "redirect:" + authRedirect;
    }

    @GetMapping("/callback")
    public String handleCallback(@RequestParam("code") String authorizationCode, Model model) {
        try {
            String accessToken = exchangeCodeForToken(authorizationCode);
            String openId = fetchOpenId(accessToken);
            JsonObject userProfile = fetchUserProfile(accessToken, openId);

            Map<String, Object> sessionData = new HashMap<>();
            sessionData.put("openId", openId);
            sessionData.put("profile", gson.fromJson(userProfile, Map.class));
            
            model.addAttribute("userData", sessionData);
            return "dashboard";
        } catch (Exception ex) {
            log.error("Error durante el flujo de autenticación de QQ", ex);
            return "redirect:/login";
        }
    }

    private String exchangeCodeForToken(String code) {
        String url = UriComponentsBuilder.fromHttpUrl(tokenUrl)
                .queryParam("grant_type", "authorization_code")
                .queryParam("client_id", clientId)
                .queryParam("client_secret", clientSecret)
                .queryParam("code", code)
                .queryParam("redirect_uri", redirectUri)
                .build().toUriString();

        String response = restTemplate.getForObject(url, String.class);
        // La API devuelve los parámetros en formato URL-encoded
        return UriComponentsBuilder.fromUriString("?" + response).build().getQueryParams().getFirst("access_token");
    }

    private String fetchOpenId(String accessToken) {
        String url = UriComponentsBuilder.fromHttpUrl(openIdUrl)
                .queryParam("access_token", accessToken)
                .build().toUriString();

        String jsonpResponse = restTemplate.getForObject(url, String.class);
        // La API devuelve un formato JSONP: callback( {"client_id":"...","openid":"..."} );
        String jsonString = jsonpResponse.replaceAll("callback\\(|\\)|;", "").trim();
        JsonObject jsonObject = gson.fromJson(jsonString, JsonObject.class);
        return jsonObject.get("openid").getAsString();
    }

    private JsonObject fetchUserProfile(String accessToken, String openId) {
        String url = UriComponentsBuilder.fromHttpUrl(userInfoUrl)
                .queryParam("access_token", accessToken)
                .queryParam("oauth_consumer_key", clientId)
                .queryParam("openid", openId)
                .build().toUriString();

        String jsonResponse = restTemplate.getForObject(url, String.class);
        return gson.fromJson(jsonResponse, JsonObject.class);
    }
}

Etiquetas: SpringBoot OAuth2 QQConnect java Thymeleaf

Publicado el 6-29 00:36