應用場景
web應用通過QQ登錄授權實現第三方登錄。
操作步驟
1 注冊成為QQ互聯平台開發者,
http://connect.qq.com/
2 准備一個可訪問的域名,如dev.foo.com
3 創建網頁應用,配置必要信息,其中包括域名以及回調地址;
其中域名需要驗證,需確保對域名主機有足夠的控制權限
4 獲取應用appID、appKey進行開發
登錄流程
開發平台的登錄授權采取oauth2.0機制,這也是目前幾乎所有互聯網開放平台所采取的方式。

需更多了解oauth2.0可參考阮老師的文章: http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
實現方式
client-side
流程:
前端頁面通過Implict方式 登錄授權 -> 回調獲得accessToken -> 獲取openid -> 同步用戶信息並登錄
為了保證數據安全,在獲取用戶信息並登錄這一步必須由服務端實現。
這種方式的開發相對便捷,也是后面的實戰案例將要采取的方式。
server-side
流程:
由server端頁面跳轉到登錄授權頁面(Authorization code方式) -> 回調獲得code -> 置換accessToken -> 獲取openid -> 同步用戶信息並登錄
SDK使用
JSSDK 可快捷實現前端登錄授權的功能,可自定制登錄按鈕
缺點:存在瀏覽器兼容風險,此外登錄按鈕UI的定制也存在受限
JavaSDK 屏蔽了oauth授權的復雜度,方便后端實現授權及api操作
缺點:增加依賴jar包,項目容易變得臃腫,尤其是當前項目已經存在oauth功能實現時可不必采用。
案例實戰
功能描述
clientside + server-side 通過QQ網頁授權登錄,並獲取用戶信息
1 本地開發環境准備
修改hosts文件將dev.foo.com映射到127.0.0.1;
本地服務器以80端口啟動, windows下可能會出現80端口被系統進程占用的情況,解決方法可參考 http://www.cnblogs.com/littleatp/p/4414578.html
本地服務器啟動后,以dev.foo.com的域名進行訪問,在QQ登錄授權時可通過域名驗證這一步
2 登錄跳轉頁面
<html> <head> <title>QQ登錄跳轉</title> <script src="http://lib.sinaapp.com/js/jquery/1.7.2/jquery.min.js" type="text/javascript"></script> <script type="text/javascript"> //切割字符串轉換參數表 function toParamMap(str){ var map = {}; var segs = str.split("&"); for(var i in segs){ var seg = segs[i]; var idx = seg.indexOf('='); if(idx < 0){ continue; } var name = seg.substring(0, idx); var value = seg.substring(idx+1); map[name] = value; } return map; } //隱式獲取url響應內容(JSONP) function openImplict(url){ var script = document.createElement('script'); script.src = url; document.body.appendChild(script); } //獲得openid的回調 function callback(obj) { var openid = obj.openid; $("#openid").text(openid); //跳轉服務端登錄url var resulturl = "@{openapi.QQs.login_result()}"; var accessToken = $("#accessToken").text(); //向服務端傳輸access_token及openid參數 document.location.href=resulturl + "?access_token=" + accessToken + "&openid=" + openid; } </script> </head> <body> <p>AccessToken:<span id="accessToken"></span>--ExpireIn<span id="expire"></span></p> <p>OpenID:<span id="openid"></span></p> <!-- 執行腳本 --> <script type="text/javascript"> //應用的APPID var appID = "101207268"; //登錄授權后的回調地址,設置為當前url var redirectURI = "@@{openapi.QQs.login()}"; //初始構造請求 if (window.location.hash.length == 0) { var path = 'https://graph.qq.com/oauth2.0/authorize?'; var queryParams = ['client_id=' + appID, 'redirect_uri=' + redirectURI, 'scope=' + 'get_user_info,list_album,upload_pic,add_feeds,do_like','response_type=token']; var query = queryParams.join('&'); var url = path + query; window.location.href= url; } //在成功授權后回調時location.hash將帶有access_token信息,開始獲取openid else { //獲取access token var accessToken = window.location.hash.substring(1); var map = toParamMap(accessToken); //記錄accessToken $("#accessToken").text(map.access_token); $("#expire").text(map.expires_in); //使用Access Token來獲取用戶的OpenID var path = "https://graph.qq.com/oauth2.0/me?"; var queryParams = ['access_token='+map.access_token, 'callback=callback']; var query = queryParams.join('&'); var url = path + query; openImplict(url); } </script> </body> </html>
功能描述
頁面在第一次打開時跳轉到QQ登錄授權頁面;
授權成功之后回到當前頁面通過url參數(hash串)獲得accessToken;
此后可通過jsonp方式獲取用戶的openid,url如:
https://graph.qq.com/oauth2.0/me?access_token=YOUR_ACCESS_TOKEN
獲取到用戶OpenID,返回包如下(JSONP方式獲取):
callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} )
將access_token及openid傳到服務端進行處理
3 server端獲取用戶信息
接收openid的頁面方法
/** * 登錄結果 * * @param access_token * @param openid */ public static void login_result(String access_token, String openid) { //調用api獲取qq用戶信息 QQUserInfo user = QQApi.getUserInfo(access_token, openid); //此時若取得user信息,則可以進行保存,並執行用戶登錄操作 .... //登錄成功后跳轉 redirect(xxx); }
QQApi的實現
/** * QQ互聯API * * <pre> * 登錄流程: * * 1 前端跳轉qq授權頁面 * 2 js獲得access_token * 3 通過jsonp方式獲得openid * 4 server端根據上傳的access_token及openid獲取用戶信息,如昵稱、頭像 * * 參考文檔: * http://wiki.connect.qq.com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_client-side#Step2.EF.BC.9A.E8.8E.B7.E5.8F.96AccessToken * </pre> * * @author xxx * @createDate 2015年3月10日 * */ public class QQApi { public static String appId = "xxx"; public static String appSecret = "xxx"; public static String baseUrl = "https://graph.qq.com"; protected static final String URL_GET_USERINFO = baseUrl + "/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s"; protected static final long ACCESS_TIMEOUT = 15; protected static final String DEF_APP_TOKEN_EXPIRE = "3h"; /** * 獲取用戶信息 * * <pre> * http://wiki.connect.qq.com/get_user_info * * * 調用地址: * https://graph.qq.com/user/get_user_info * 參數 * access_token=*************& * oauth_consumer_key=12345& * openid * * 返回結果如下: * { * "ret": 0, * "msg": "", * "is_lost": 0, * "nickname": "小吞", * "gender": "女", * "province": "廣東", * "city": "廣州", * "year": "1993", * "figureurl": "http://qzapp.qlogo.cn/qzapp/101207268/982C9FEADAF7B242C5069B8F390784BF/30", * "figureurl_1": "http://qzapp.qlogo.cn/qzapp/101207268/982C9FEADAF7B242C5069B8F390784BF/50", * "figureurl_2": "http://qzapp.qlogo.cn/qzapp/101207268/982C9FEADAF7B242C5069B8F390784BF/100", * "figureurl_qq_1": "http://q.qlogo.cn/qqapp/101207268/982C9FEADAF7B242C5069B8F390784BF/40", * "figureurl_qq_2": "http://q.qlogo.cn/qqapp/101207268/982C9FEADAF7B242C5069B8F390784BF/100", * "is_yellow_vip": "0", * "vip": "0", * "yellow_vip_level": "0", * "level": "0", * "is_yellow_year_vip": "0" * } * </pre> * * @param accessToken * @return */ public static QQUserInfo getUserInfo(String accessToken, String openid) { if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(openid)) { return null; } String url = String.format(URL_GET_USERINFO, accessToken, appId, openid); String resultString = DefaultHttp.get(url, ACCESS_TIMEOUT, GlobalConstants.UTF_8); Logger.debug("[sso-qq]get userinfo. use url '%s'", url); QQUserInfo userinfo = JsonUtil.fromJson(resultString, QQUserInfo.class); if (userinfo == null || !userinfo.hasGot()) { Logger.debug("[sso-qq]get userinfo failed, with result of '%s'", resultString); return null; } Logger.debug("[sso-qq]get userinfo success, with result of '%s'", resultString); return userinfo; }
常見問題
網頁跳轉提示 "redirect_uri_mismatch"
通常是應用配置中的域名與當前開發服務器訪問地址不一致導致,參照案例中的本地開發環境准備小節
api調用返回錯誤
查看返回的ret字段,對於非0值的ret則表示異常結果,可通過以下地址查詢錯誤原因:
接口調用過於頻繁或超過限制
應用系統可做好access_token的存儲,此外對於用戶數據(昵稱、頭像)也做好緩存或持久化,以減少接口的調用頻度。