騰訊開放平台(QQ 互聯)提供基於 OAuth 2.0 的第三方登入服務,讓用戶無需註冊即可使用 QQ 賬號登入您的應用。本文提供生產級整合代碼,涵蓋授權碼模式完整四步流程、Token 刷新機制、OpenID 獲取、用戶資料拉取,並處理異常重試、狀態防偽等安全細節。
┌─────────┐ 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;
}
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);
}
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);
}
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");
}
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 ""; // 刷新失敗,需重新授權
}
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) : "";
}
# 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
@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";
}
}
}
100010(Token 過期)、100030(Refresh Token 過期)自動觸發刷新或重新授權display=mobile 參數控制授權頁面樣式URLEncoder.encode(..., "UTF-8")完整整合騰訊 OAuth 2.0 登入,關鍵在於流程串接正確、異常情況全覆蓋、安全細節零疏漏。建議先在測試環境跑通全流程,再逐步加入緩存、重試、監控等生產級能力。