前幾天隨手寫了一個qq第三方授權登錄功能,現總結一下(這里以個人開發網站應用為例):
首先要成為qq互聯開發者:https://connect.qq.com/index.html申請步驟請參考文檔和百度:https://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85等待審核通過,通過之后會看到(示例):
然后開始創應用,我這邊是網站應用,創建流程請參考文檔https://wiki.connect.qq.com/__trashed-2和百度,創建過程中請注意網站域名回調和備案號要和備案信息一致!資料填寫完最多7個工作日就可以審核完成,完成之后為(示例):
點擊查看主要是要拿到應用的APPID和APPKEY(示例):
基本信息都拿到之后請仔細閱讀開發文檔查看需要用到的sdk及api開始進行開發。我這邊的后端架構用的是springboot2.2.x+mybatis annotation 前端用的是layui,不過基本邏輯都一樣:
首先將一些固定數據 APPID(網站應用審核通過后獲取的APPID),APPKEY(網站應用審核通過后獲取的APPKEY),DOMAIN(申請的域名),CALLBACK(回調地址)寫到配置文件或者靜態資源類中去,方便修改和調用(在網站應用中的網站回調域可以加一個測試的地址,例如:http://127.0.0.1:8080/callback[callback是示例,具體回調地址以自己為准])。
示例:
1 public class QQWikiParamter { 2 3 public static final String APPID = "<yourAppId>"; 4 5 public static final String APPKEY = "<yourAppKey>"; 6 7 //public static final String DOMAIN = "http://127.0.0.1:8080"; 8 9 public static final String DOMAIN = "<yourDomain>"; 10 11 public static final String REDIRECT_URL = "<yourCallBack>"; 12 }
通過查看文檔得知我們需要用到一些api,由此封裝一個工具類commonUtil:

1 import net.sf.json.JSONException; 2 import net.sf.json.JSONObject; 3 import org.apache.commons.lang3.StringUtils; 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 7 import javax.net.ssl.HttpsURLConnection; 8 import javax.net.ssl.SSLContext; 9 import javax.net.ssl.SSLSocketFactory; 10 import javax.net.ssl.TrustManager; 11 import java.io.BufferedReader; 12 import java.io.InputStream; 13 import java.io.InputStreamReader; 14 import java.io.OutputStream; 15 import java.net.ConnectException; 16 import java.net.URL; 17 18 /** 19 * @author kabuqinuo 20 * @date 2020/2/18 12:50 21 */ 22 public class CommonUtil { 23 24 private static Logger log = LoggerFactory.getLogger(CommonUtil.class); 25 26 //獲取Authorization Code 27 public final static String auth_url = "https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=APPID&redirect_uri=REDIRECTURL&&state=STATE"; 28 // 憑證獲取(GET) 29 public final static String token_url = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=APPID&client_secret=APPSECRET&code=CODE&redirect_uri=REDIRECTURL"; 30 //權限自動續期,獲取Access Token 31 public final static String refresh_token_url = "https://graph.qq.com/oauth2.0/token?grant_type=refresh_token&client_id=APPID&client_secret=APPSECRET&refresh_token=REFRESHTOKEN"; 32 //獲取用戶OpenID_OAuth2.0 33 public final static String oauth_url = "https://graph.qq.com/oauth2.0/me?access_token=ACCESSTOKEN"; 34 //獲取登錄用戶的昵稱、頭像、性別 35 public final static String user_info_url = "https://graph.qq.com/user/get_user_info?access_token=ACCESSTOKEN&oauth_consumer_key=APPID&openid=OPENID"; 36 37 38 /** 39 * 發送https請求 40 * 41 * @param requestUrl 請求地址 42 * @param requestMethod 請求方式(GET、POST) 43 * @param outputStr 提交的數據 44 * @return JSONObject(通過JSONObject.get(key)的方式獲取json對象的屬性值) 45 */ 46 public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) { 47 String result = null; 48 try { 49 // 創建SSLContext對象,並使用我們指定的信任管理器初始化 50 TrustManager[] tm = { new MyX509TrustManager() }; 51 SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); 52 sslContext.init(null, tm, new java.security.SecureRandom()); 53 // 從上述SSLContext對象中得到SSLSocketFactory對象 54 SSLSocketFactory ssf = sslContext.getSocketFactory(); 55 56 URL url = new URL(requestUrl); 57 HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); 58 conn.setSSLSocketFactory(ssf); 59 60 conn.setDoOutput(true); 61 conn.setDoInput(true); 62 conn.setUseCaches(false); 63 // 設置請求方式(GET/POST) 64 conn.setRequestMethod(requestMethod); 65 66 // 當outputStr不為null時向輸出流寫數據 67 if (null != outputStr) { 68 OutputStream outputStream = conn.getOutputStream(); 69 // 注意編碼格式 70 outputStream.write(outputStr.getBytes("UTF-8")); 71 outputStream.close(); 72 } 73 74 // 從輸入流讀取返回內容 75 InputStream inputStream = conn.getInputStream(); 76 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); 77 BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 78 String str = null; 79 StringBuffer buffer = new StringBuffer(); 80 while ((str = bufferedReader.readLine()) != null) { 81 buffer.append(str); 82 } 83 84 // 釋放資源 85 bufferedReader.close(); 86 inputStreamReader.close(); 87 inputStream.close(); 88 conn.disconnect(); 89 result = buffer.toString(); 90 } catch (ConnectException ce) { 91 log.error("連接超時:{}", ce); 92 } catch (Exception e) { 93 log.error("https請求異常:{}", e); 94 } 95 return result; 96 } 97 98 /** 99 * 獲取Authorization Code 100 * @param appid 101 * @param redirect_url 102 * @param state 103 * @return 104 */ 105 public static String getCode(String appid, String redirect_url, String state){ 106 String resUrl = auth_url.replace("APPID",appid).replace("REDIRECTURL",redirect_url).replace("STATE",state); 107 return resUrl; 108 } 109 110 /** 111 * 獲取接口訪問憑證 112 * @param appid 113 * @param appsecret 114 * @param code 115 * @param redirect_url 116 * @return 117 */ 118 public static Token getToken(String appid, String appsecret, String code, String redirect_url) { 119 Token token = null; 120 String requestUrl = token_url.replace("APPID", appid).replace("APPSECRET", appsecret).replace("CODE",code).replace("REDIRECTURL",redirect_url); 121 // 發起GET請求獲取憑證 122 String content = httpsRequest(requestUrl, "GET", null); 123 if (StringUtils.isNotBlank(content)){ 124 content = content.replace("=","\":\""); 125 content = content.replace("&","\",\""); 126 content = "{\"" + content +"\"}"; 127 JSONObject jsonObject = JSONObject.fromObject(content); 128 129 if (null != jsonObject) { 130 try { 131 token = new Token(); 132 token.setAccessToken(jsonObject.getString("access_token")); 133 token.setExpiresIn(jsonObject.getInt("expires_in")); 134 token.setRefreshToken(jsonObject.getString("refresh_token")); 135 } catch (JSONException e) { 136 token = null; 137 // 獲取token失敗 138 log.error("獲取token失敗 errcode:{} errmsg:{}", jsonObject.getInt("code"), jsonObject.getString("msg")); 139 } 140 } 141 } 142 return token; 143 } 144 145 /** 146 * 權限自動續期,獲取Access Token 147 * @param appid 148 * @param appsecret 149 * @param refresh_token 150 * @return 151 */ 152 public static Token getRefreshToken(String appid, String appsecret,String refresh_token) { 153 Token token = null; 154 String requestUrl = refresh_token_url.replace("APPID", appid).replace("APPSECRET", appsecret).replace("REFRESHTOKEN",refresh_token); 155 // 發起GET請求獲取憑證 156 String content = httpsRequest(requestUrl, "GET", null); 157 if (StringUtils.isNotBlank(content)){ 158 content = content.replace("=","\":\""); 159 content = content.replace("&","\",\""); 160 content = "{\"" + content +"\"}"; 161 JSONObject jsonObject = JSONObject.fromObject(content); 162 163 if (null != jsonObject) { 164 try { 165 token = new Token(); 166 token.setAccessToken(jsonObject.getString("access_token")); 167 token.setExpiresIn(jsonObject.getInt("expires_in")); 168 token.setRefreshToken(jsonObject.getString("refresh_token")); 169 } catch (JSONException e) { 170 token = null; 171 // 獲取token失敗 172 log.error("獲取token失敗 errcode:{} errmsg:{}", jsonObject.getInt("code"), jsonObject.getString("msg")); 173 } 174 } 175 } 176 return token; 177 } 178 179 /** 180 * 獲取用戶OpenID_OAuth2.0 181 * @param access_token 182 * @return 183 */ 184 public static Me getMe(String access_token){ 185 Me me = null; 186 String requestUrl = oauth_url.replace("ACCESSTOKEN",access_token); 187 188 // 發起GET請求獲取憑證 189 String result = httpsRequest(requestUrl, "GET", null); 190 if (StringUtils.isNotBlank(result)) { 191 try { 192 me = new Me(); 193 me.setOpenId(StringUtils.substringBetween(result, "\"openid\":\"", "\"}")); 194 } catch (JSONException e){ 195 me = null; 196 log.error("獲取用戶信息失敗 "); 197 } 198 } 199 return me; 200 } 201 202 /** 203 * 獲取登錄用戶的昵稱、頭像、性別 204 * @param access_token 205 * @param appid 206 * @param openid 207 * @return 208 */ 209 public static String getUserInfo(String access_token, String appid, String openid){ 210 String result = null; 211 String requestUrl = user_info_url.replace("ACCESSTOKEN",access_token).replace("APPID",appid).replace("OPENID",openid); 212 213 // 發起GET請求獲取憑證 214 result= httpsRequest(requestUrl, "GET", null); 215 if (StringUtils.isNotBlank(result)) { 216 return result; 217 } 218 return result; 219 } 220 }
工具類涉及到的實體類示例代碼:

1 /** 2 * 信任管理器 3 * @author kabuqinuo 4 * @date 2020/2/18 12:54 5 */ 6 public class MyX509TrustManager implements X509TrustManager { 7 8 // 檢查客戶端證書 9 @Override 10 public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { 11 12 } 13 14 // 檢查服務器端證書 15 @Override 16 public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { 17 18 } 19 20 // 返回受信任的X509證書數組 21 @Override 22 public X509Certificate[] getAcceptedIssuers() { 23 return new X509Certificate[0]; 24 } 25 }

1 @Data 2 public class Token implements Serializable { 3 4 private String accessToken; 5 6 private Integer expiresIn; 7 8 private String refreshToken; 9 }

1 @Data 2 public class Me implements Serializable { 3 4 private String openId; 5 }
還有一個問題是qq互聯登錄可能會涉及到跨域,請注意跨域配置,示例后端springboot配置跨域:

1 @Configuration 2 public class CorsConfig { 3 4 private CorsConfiguration buildConfig() { 5 CorsConfiguration corsConfiguration = new CorsConfiguration(); 6 corsConfiguration.addAllowedOrigin("*"); // 1允許任何域名使用 7 corsConfiguration.addAllowedHeader("*"); // 2允許任何頭 8 corsConfiguration.addAllowedMethod("*"); // 3允許任何方法(post、get等) 9 return corsConfiguration; 10 } 11 12 @Bean 13 public CorsFilter corsFilter() { 14 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 15 source.registerCorsConfiguration("/**", buildConfig()); // 4 16 return new CorsFilter(source); 17 } 18 }
准備工作做完現在開始進行開發:
首先是前端:
前端頁面上需要訪問一個qq第三方登錄的入口按鈕,當然按鈕就是一個qq圖標,這里qq喚起授權頁面有兩種類型,一種是官網上的點擊qq登錄圖標訪問鏈接在新窗口打開授權頁面;一種是點擊qq登錄圖標在本窗口跳轉授權頁面;
我用的是官網上的這一種方式,在說以前簡單的講一下本窗口打開授權頁面邏輯:
本窗口打開授權頁面可以直接在圖標所在的標簽上跳轉api接口訪問后端,后端重定向到qq授權頁面(示例):
1 <a href="/info/wiki" class="seraph icon-qq wiki-qq layui-col-xs6 layui-col-sm6 layui-col-md4 layui-col-lg6 layui-icon layui-icon-login-qq" ></a>
我這個a標簽在頁面上就是qq圖標的代碼,a標簽直接訪問后端接口:
1 @GetMapping(value = "/wiki") 2 @IgnoreSecurity 3 public void toLogin(HttpServletRequest request, HttpServletResponse response) { 4 String state=ToolsBarUtil.getRandomString(10); 5 //重定向 6 String url = CommonUtil.getCode(QQWikiParamter.APPID, QQWikiParamter.DOMAIN + QQWikiParamter.REDIRECT_URL, state); 7 try { 8 response.sendRedirect(url); 9 } catch (IOException e) { 10 e.printStackTrace(); 11 } 12 }
此時就點擊就可以打開qq授權頁面了(示例):
第二種也就是我現在用的這種點擊qq登錄在新窗口打開一個授權頁面:
寫一個點擊事件,在點擊觸發事件里面打開授權頁面,示例:
1 var childWindow; 2 $(".loginBody .wiki-qq").on("click",function(){ 3 childWindow = window.open("/info/wiki","TencentLogin", 4 "width=550,height=320,menubar=1,scrollbars=1, resizable=1,status=1,titlebar=0,toolbar=0,location=1"); 5 })
這樣就會在新的窗口打開一個qq授權頁面,如果想改變授權頁面窗口大小請參考百度,后端wiki接口代碼一致。文檔參考:https://wiki.connect.qq.com/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token step1:Authorization Code
授權之后就是qq授權回調事件了,授權成功之后會跳轉到qq互聯后台設置的授權回調地址里面,文檔參考:https://wiki.connect.qq.com/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token step2:通過Authorization Code獲取Access Token
代碼示例:

1 @GetMapping(value = "/callback") 2 @IgnoreSecurity 3 public ModelAndView wiki(ModelAndView mvc, HttpServletRequest request, HttpServletResponse response) throws IOException { 4 String code = request.getParameter("code"); 5 String state = request.getParameter("state"); 6 if (state == null || code == null) { 7 mvc.addObject("msg","授權失敗"); 8 mvc.setViewName("login"); 9 return mvc; 10 } 11 //獲取access_token 12 Token token = CommonUtil.getToken(QQWikiParamter.APPID, QQWikiParamter.APPKEY, code, QQWikiParamter.DOMAIN + QQWikiParamter.REDIRECT_URL); 13 if (StringUtils.isNotBlank(token.getAccessToken())){ 14 //獲取用戶openid 15 Me me = CommonUtil.getMe(token.getAccessToken()); 16 if(StringUtils.isNotBlank(me.getOpenId())){ 17 //獲取用戶信息 18 String user_info = CommonUtil.getUserInfo(token.getAccessToken(), QQWikiParamter.APPID, me.getOpenId()); 19 20 //獲取到用戶信息之后自己的處理邏輯..... 21 22 String url = "/info/login?"+ URLEncoder.encode(Base64.getBase64("openId"),"UTF-8")+"="+URLEncoder.encode(Base64.getBase64(me.getOpenId()),"UTF-8")+"&dis="+URLEncoder.encode(Base64.getBase64(ToolsBarUtil.getRandomString(10)),"UTF-8"); 23 mvc.addObject("url",url); 24 } 25 } 26 mvc.setViewName("qqCallBack"); 27 return mvc; 28 }
這里說明一下:
如果是本窗口打開的授權頁面,這里獲取到用戶信息之后處理完自己的邏輯可以直接跳轉到首頁去。因為我這邊是在新窗口打開的授權頁面,所以需要一個中間頁面處理回調。
然后是qqCallBack頁面處理(提示:我在callback接口中把獲取到的openId通過加密放到了url中並帶到的回調頁面。)示例代碼:
1 <body> 2 <div class="parentPage" style="display:none;"> 3 <input type="text" id="msg" name="msg" th:value="${msg}"></div> 4 <input type="text" id="url" name="url" th:value="${url}"> 5 <span>登錄成功</span> 6 </body> 7 <script type="text/javascript"> 8 $(document).ready(function () { 9 var msg = $('#msg').val(); 10 if (msg != null && msg != ''){ 11 layer.msg("qq登錄授權失敗!"); 12 return; 13 } 14 var url = $('#url').val(); 15 window.close(); 16 window.opener.location.href=url; 17 }); 18 </script>
處理邏輯說明:
授權成功后跳轉到qqCallback授權頁面,授權頁面判斷是否授權失敗,如無,就關閉當前頁面並刷新前一個頁面(在這邊前一個頁面就是我的登錄頁面,也就是url,不過此時的url后面帶有加密過的token)
此時授權頁面關閉,發起授權的頁面刷新並帶有參數加密的token,刷新的授權頁面的時候就發起一個ajax,通過頁面獲取的token從后端接口中查詢是否已qq授權成功,查詢邏輯按照自己的想法寫。
此時qq第三方授權登錄基本邏輯完成。
代碼僅僅是授權邏輯,具體業務不涉及,僅供參考。