首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

騰訊 OAuth 2.0 聯合登錄整合指南:從授權碼到用戶資料完整流程

     編輯:Java 綜合教學

QQ 互聯 OAuth 2.0 整合完整實戰教程

騰訊開放平台(QQ 互聯)提供基於 OAuth 2.0 的第三方登入服務,讓用戶無需註冊即可使用 QQ 賬號登入您的應用。本文提供生產級整合代碼,涵蓋授權碼模式完整四步流程、Token 刷新機制、OpenID 獲取、用戶資料拉取,並處理異常重試、狀態防偽等安全細節。

OAuth 2.0 授權碼模式四步流程圖解

┌─────────┐     1. 導向授權頁      ┌──────────────┐
│ 用戶瀏覽器 │ ──────────────────▶ │ 騰訊授權服務器  │
└─────────┘   response_type=code  └──────────────┘
      ▲                                    │
      │         2. 重定向回調               │
      │   code=xxx&state=xxx                │
      │                                    ▼
┌─────────┐     3. 換取 Access Token  ┌──────────────┐
│ 您的後端  │ ◀────────────────── │ 騰訊 Token 端點 │
└─────────┘   grant_type=auth_code  └──────────────┘
      │                                    │
      │         4. 獲取用戶資料             │
      └──────────────────▶ ┌──────────────┐
                           │ 騰訊 OpenAPI  │
                           └──────────────┘

核心配置參數

@Component
public class QQUnionLoginApi {
    @Value("${QQ_CONSUMER_KEY}")        // App ID
    private String appKey;
    
    @Value("${QQ_CONSUMER_SECRET}")     // App Key
    private String appSecret;
    
    @Value("${QQ_LOGIN_URL}")           // https://graph.qq.com/oauth2.0/authorize
    private String loginUrl;
    
    @Value("${QQ_ACCESS_TOKEN_URL}")    // https://graph.qq.com/oauth2.0/token
    private String accessTokenUrl;
    
    @Value("${QQ_OPEN_ID_URL}")         // https://graph.qq.com/oauth2.0/me
    private String openIdUrl;
    
    @Value("${QQ_USER_INFO_URL}")       // https://graph.qq.com/user/get_user_info
    private String userInfoUrl;
    
    @Value("${QQ_UNION_LOGIN_CALLBACK_PATH}") // 回調地址
    private String callbackPath;
}

步驟一:構建授權頁面 URL

public String getQQLoginUrl() {
    StringBuilder url = new StringBuilder(loginUrl);
    url.append("?response_type=code");                    // 固定值
    url.append("&client_id=").append(appKey);             // App ID
    url.append("&redirect_uri=").append(URLEncoder.encode(callbackPath, "UTF-8")); // 回調地址
    url.append("&scope=get_user_info");                   // 申請權限範圍
    url.append("&state=").append(generateState());        // CSRF 防偽隨機字串
    url.append("&display=pc");                            // pc/mobile 適配
    return url.toString();
}

// 生成防偽 state,建議存入 Session 驗證
private String generateState() {
    return "qq_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}

步驟二:授權碼換取 Access Token

public String getQQAccessToken(String authorizationCode) throws IOException {
    StringBuilder url = new StringBuilder(accessTokenUrl);
    url.append("?grant_type=authorization_code");
    url.append("&client_id=").append(appKey);
    url.append("&client_secret=").append(appSecret);
    url.append("&code=").append(authorizationCode);
    url.append("&redirect_uri=").append(URLEncoder.encode(callbackPath, "UTF-8"));
    url.append("&state=").append(generateState());
    
    String response = doGet(url.toString());
    // 響應格式:access_token=xxx&expires_in=7776000&refresh_token=yyy
    if (response.contains("access_token")) {
        return extractParam(response, "access_token");
    }
    throw new QQApiException("獲取 Access Token 失敗: " + response);
}

步驟三:Access Token 獲取 OpenID

public String getQQOpenId(String accessToken) throws IOException {
    String url = openIdUrl + "?access_token=" + accessToken;
    String response = doGet(url);
    // 響應格式:callback( {"client_id":"APPID","openid":"OPENID"} );
    int start = response.indexOf("{");
    int end = response.lastIndexOf("}") + 1;
    String json = response.substring(start, end);
    
    Map map = JsonUtils.json2Object(json, HashMap.class);
    return map.get("openid");
}

步驟四:獲取用戶資料

public String getUserNickName(String accessToken, String openId) throws IOException {
    StringBuilder url = new StringBuilder(userInfoUrl);
    url.append("?oauth_consumer_key=").append(appKey);
    url.append("&access_token=").append(accessToken);
    url.append("&openid=").append(openId);
    
    String response = doGet(url.toString());
    Map map = JsonUtils.json2Object(response, HashMap.class);
    return map.get("nickname");
}

Token 刷新機制(Access Token 有效期 90 天)

public String refreshQQAccessToken(String refreshToken) throws IOException {
    StringBuilder url = new StringBuilder(accessTokenUrl);
    url.append("?grant_type=refresh_token");
    url.append("&client_id=").append(appKey);
    url.append("&client_secret=").append(appSecret);
    url.append("&refresh_token=").append(refreshToken);
    
    String response = doGet(url.toString());
    if (response.contains("access_token")) {
        return extractParam(response, "access_token");
    }
    return ""; // 刷新失敗,需重新授權
}

HTTP 請求工具方法

private String doGet(String url) throws IOException {
    log.info("QQ API 請求: {}", url);
    HttpResponseWrapper wrapper = HttpsUtil.requestGetResponse(url);
    
    if (!HttpsUtil.isCallOK(wrapper.getStatusCode())) {
        throw new IOException("HTTP 錯誤碼: " + wrapper.getStatusCode());
    }
    
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(wrapper.getResponseStream(), StandardCharsets.UTF_8))) {
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line.replace("\t", ""));
        }
        log.info("QQ API 響應: {}", sb);
        return sb.toString();
    } finally {
        wrapper.close();
    }
}

private String extractParam(String response, String paramName) {
    String pattern = paramName + "=([^&]+)";
    Pattern p = Pattern.compile(pattern);
    Matcher m = p.matcher(response);
    return m.find() ? m.group(1) : "";
}

Spring Boot 整合完整配置

# application.yml
qq:
  consumer-key: 101xxxxxx
  consumer-secret: xxxxxxxxxxxxxxxxxxxxxxxx
  login-url: https://graph.qq.com/oauth2.0/authorize
  access-token-url: https://graph.qq.com/oauth2.0/token
  open-id-url: https://graph.qq.com/oauth2.0/me
  user-info-url: https://graph.qq.com/user/get_user_info
  union-login-callback-path: https://yourdomain.com/auth/qq/callback

Controller 端點實現

@Controller
@RequestMapping("/auth/qq")
public class QQAuthController {
    
    @Autowired private QQUnionLoginApi qqApi;
    
    @GetMapping("/login")
    public String login(HttpSession session) {
        String state = qqApi.generateState();
        session.setAttribute("qq_state", state); // 存儲 state 防 CSRF
        return "redirect:" + qqApi.getQQLoginUrl();
    }
    
    @GetMapping("/callback")
    public String callback(@RequestParam String code, 
                          @RequestParam String state,
                          HttpSession session,
                          Model model) {
        // 驗證 state 防 CSRF
        String savedState = (String) session.getAttribute("qq_state");
        if (!state.equals(savedState)) {
            return "error/illegal_state";
        }
        
        try {
            String accessToken = qqApi.getQQAccessToken(code);
            String openId = qqApi.getQQOpenId(accessToken);
            String nickname = qqApi.getUserNickName(accessToken, openId);
            
            // 這裡進行用戶綁定/註冊/登入邏輯
            User user = userService.bindOrRegister(openId, nickname, "QQ");
            session.setAttribute("currentUser", user);
            
            return "redirect:/dashboard";
        } catch (Exception e) {
            log.error("QQ 登入失敗", e);
            return "error/auth_failed";
        }
    }
}

安全與異常處理清單

  1. State 參數驗證:必須在 Session 中保存並驗證,防止 CSRF 攻擊
  2. HTTPS 強制:生產環境回調地址必須為 HTTPS
  3. Token 安全存儲:Access Token、Refresh Token 加密存入資料庫,勿寫入日誌
  4. 異常重試策略:網絡超時採用指數退避重試(最多 3 次)
  5. 頻率限制:騰訊 API 有調用頻率限制,需本地緩存用戶資料
  6. 錯誤碼處理:針對 100010(Token 過期)、100030(Refresh Token 過期)自動觸發刷新或重新授權

常見坑點避雷

  • 回調域名驗證:騰訊後台配置的回調域名必須與實際完全一致(含協議、端口、路徑)
  • 移動端適配:display=mobile 參數控制授權頁面樣式
  • OpenID 唯一性:同一用戶在不同應用的 OpenID 不同,需用 UnionID 體系(需申請權限)
  • 編碼問題:所有 URL 參數必須 URLEncoder.encode(..., "UTF-8")

完整整合騰訊 OAuth 2.0 登入,關鍵在於流程串接正確、異常情況全覆蓋、安全細節零疏漏。建議先在測試環境跑通全流程,再逐步加入緩存、重試、監控等生產級能力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved