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);
}
}