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第三方授权登录基本逻辑完成。

代码仅仅是授权逻辑,具体业务不涉及,仅供参考。

posted @ 2020-02-25 11:48  {{unidentified}}  阅读(1516)  评论(0编辑  收藏  举报