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 }
工具类涉及到的实体类示例代码:
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第三方授权登录基本逻辑完成。
代码仅仅是授权逻辑,具体业务不涉及,仅供参考。