springboot開發qq第三方授權登錄


前幾天隨手寫了一個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 }
View Code

工具類涉及到的實體類示例代碼:

 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 }
View Code
1 @Data
2 public class Token implements Serializable {
3 
4     private String accessToken;
5 
6     private Integer expiresIn;
7 
8     private String refreshToken;
9 }
View Code
1 @Data
2 public class Me implements Serializable {
3 
4     private String openId;
5 }
View Code

還有一個問題是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 }
View Code

准備工作做完現在開始進行開發:

首先是前端:

前端頁面上需要訪問一個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     }
View Code

這里說明一下:

如果是本窗口打開的授權頁面,這里獲取到用戶信息之后處理完自己的邏輯可以直接跳轉到首頁去。因為我這邊是在新窗口打開的授權頁面,所以需要一個中間頁面處理回調。

然后是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第三方授權登錄基本邏輯完成。

代碼僅僅是授權邏輯,具體業務不涉及,僅供參考。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM