Intégration de l'authentification QQ dans une application Spring Boot

Intégration de l'authentification QQ dans une application Spring Boot

Considérations préliminaires sur l'architecture

Avant de commencer l'intégration technique, réfléchissons à la conception backend. Lors de l'implémentation d'une connexion via un service tiers, la gestion des informations utilisateur est primordiale. Un point essentiel : après une authentification réussie, nous n'obtenons qu'un identifiant unique (UID réel pour Weibo, OpenID chiffré pour QQ) ainsi qu'un token d'accès pour valider l'identité. D'autres données limitées comme le pseudo, l'avatar et le genre sont également récupérées.

Le défi principal réside dans la vérification de la légitimité de la connexion et l'identification constante de l'utilisateur entre deux sessions. Ce mécanisme est généralement assuré par le protocole OAuth2.0 : après une authentification réussie, le service tiers fournit un code temporaire. Ce code, échangé contre un access_token, valide l'auhtenticité de la requête initiale.

Prérequis fondamentaux

1. Protocole OAuth2.0 : La majorité des intégrations de connexion tierces reposent sur ce protocole. Il est fortement recommandé d'en étudier les principes avant l'implémentation.

2. Domaine avec certification : L'URL de rappel (callback) doit correspondre à un domaine certifié. Par exemple : https://mon-domaine.com/.

3. Enregistrement sur QQ Connect : Créer un compte développeur sur la plateforme QQ Connect (https://connect.qq.com) et compléter la vérification d'identité requise.

Configuration de l'application Spring Boot

Dépendances Maven

<dependencies>
    <!-- Client HTTP -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
        <version>5.2</version>
    </dependency>
    
    <!-- Traitement JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
    <!-- Configuration YAML -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- Serveur Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
</dependencies>

Classe utilitaire pour les requêtes HTTP

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class RestServiceClient {
    
    private final ObjectMapper jsonMapper = new ObjectMapper();
    
    public String executeGetRequest(String url, Map<String, String> queryParams) throws Exception {
        String query = queryParams.entrySet().stream()
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining("&"));
        
        String fullUrl = url.contains("?") ? url + "&" + query : url + "?" + query;
        
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet request = new HttpGet(fullUrl);
            return client.execute(request, response -> {
                return EntityUtils.toString(response.getEntity());
            });
        }
    }
    
    public <T> T parseJson(String content, Class<T> valueType) throws Exception {
        return jsonMapper.readValue(content, valueType);
    }
}

Configuration des propriétés d'authentification

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.validation.constraints.NotBlank;

@Configuration
@ConfigurationProperties(prefix = "oauth.providers.qq")
public class QqOAuthConfiguration {
    
    @NotBlank
    private String appId;
    @NotBlank
    private String appKey;
    @NotBlank
    private String redirectEndpoint;
    private String authorizationEndpoint = "https://graph.qq.com/oauth2.0/authorize";
    private String tokenEndpoint = "https://graph.qq.com/oauth2.0/token";
    private String userInfoEndpoint = "https://graph.qq.com/user/get_user_info";
    
    // Getters et Setters
    public String getAppId() { return appId; }
    public void setAppId(String appId) { this.appId = appId; }
    public String getAppKey() { return appKey; }
    public void setAppKey(String appKey) { this.appKey = appKey; }
    public String getRedirectEndpoint() { return redirectEndpoint; }
    public void setRedirectEndpoint(String redirectEndpoint) { this.redirectEndpoint = redirectEndpoint; }
    public String getAuthorizationEndpoint() { return authorizationEndpoint; }
    public String getTokenEndpoint() { return tokenEndpoint; }
    public String getUserInfoEndpoint() { return userInfoEndpoint; }
}

Contrôleur d'authentification

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/auth/qq")
public class QqAuthenticationController {
    
    @Autowired
    private QqOAuthConfiguration qqConfig;
    
    @Autowired
    private RestServiceClient httpClient;
    
    @GetMapping("/initiate")
    public RedirectView startAuthentication() {
        Map<String, String> parameters = new HashMap<>();
        parameters.put("response_type", "code");
        parameters.put("client_id", qqConfig.getAppId());
        parameters.put("redirect_uri", qqConfig.getRedirectEndpoint());
        parameters.put("state", UUID.randomUUID().toString());
        
        String authUrl = qqConfig.getAuthorizationEndpoint() + "?" + 
            parameters.entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.joining("&"));
        
        return new RedirectView(authUrl);
    }
    
    @GetMapping("/callback")
    public ResponseEntity<UserProfile> handleCallback(@RequestParam String code) {
        try {
            // Étape 1 : Obtenir le token d'accès
            Map<String, String> tokenParams = Map.of(
                "grant_type", "authorization_code",
                "client_id", qqConfig.getAppId(),
                "client_secret", qqConfig.getAppKey(),
                "code", code,
                "redirect_uri", qqConfig.getRedirectEndpoint()
            );
            
            String tokenResponse = httpClient.executeGetRequest(
                qqConfig.getTokenEndpoint(), 
                tokenParams
            );
            AccessTokenResponse tokenData = httpClient.parseJson(
                tokenResponse, 
                AccessTokenResponse.class
            );
            
            // Étape 2 : Récupérer l'identifiant ouvert
            String openidResponse = httpClient.executeGetRequest(
                "https://graph.qq.com/oauth2.0/me",
                Map.of("access_token", tokenData.accessToken())
            );
            
            // Traitement de la réponse JSON encapsulée
            String jsonContent = openidResponse.substring(
                openidResponse.indexOf("{"),
                openidResponse.lastIndexOf("}") + 1
            );
            OpenIdData openidData = httpClient.parseJson(jsonContent, OpenIdData.class);
            
            // Étape 3 : Récupérer le profil utilisateur
            Map<String, String> profileParams = Map.of(
                "access_token", tokenData.accessToken(),
                "openid", openidData.openid(),
                "oauth_consumer_key", qqConfig.getAppId()
            );
            
            String profileResponse = httpClient.executeGetRequest(
                qqConfig.getUserInfoEndpoint(),
                profileParams
            );
            
            UserProfile profile = httpClient.parseJson(profileResponse, UserProfile.class);
            return ResponseEntity.ok(profile);
            
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
    
    // Classes internes pour la structure de données
    record AccessTokenResponse(String accessToken, String expiresIn, String refreshToken) {}
    record OpenIdData(String openid, String clientId) {}
    record UserProfile(
        String nickname,
        String figureurlQq1,
        String figureurlQq2,
        String gender,
        String province,
        String city,
        String vip,
        String yellowVipLevel
    ) {}
}

Configuration de sécurité CORS

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
public class CorsConfiguration implements WebFluxConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/auth/**")
            .allowedOrigins("*")
            .allowedMethods("GET", "POST")
            .allowedHeaders("*")
            .maxAge(3600);
    }
}

Configuration application.yml

oauth:
  providers:
    qq:
      app-id: VOTRE_APP_ID
      app-key: VOTRE_APP_SECRET
      redirect-endpoint: https://votre-domaine.com/auth/qq/callback

server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: VOTRE_MOT_DE_PASSE
    key-store-type: PKCS12

Considérations de déploiement

Pour le fonctionnement correct du flux OAuth, l'application doit être accessible via une URL HTTPS. En développement local, il est possible d'utiliser un outil comme ngrok pour créer un tunnel sécurisé. Les étapes de déploiement incluent :

  1. Emballage de l'application : mvn clean package
  2. Déploiement sur un serveur avec certificat SSL valide
  3. Configuration des enregistrements DNS
  4. Test du flux d'authentification complet

Étiquettes: oauth2 QQ-Login spring-boot Java WebFlux

Publié le 22 juillet à 03h30