zoukankan      html  css  js  c++  java
  • 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第三方授权登录基本逻辑完成。

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

  • 相关阅读:
    OpenGL(九) 三维混色和深度缓存设置
    自由度(degree of freedom)
    自由度(degree of freedom)
    非参贝叶斯(Bayesian Non-parameter)初步
    非参贝叶斯(Bayesian Non-parameter)初步
    一个GCC4.6.3的奇妙问题的糊涂解决方案
    Rational Rose--简介
    android 国际化
    日志文件C++ 时间 文件 行数
    看原理图之UART
  • 原文地址:https://www.cnblogs.com/unidentified/p/12360872.html
Copyright © 2011-2022 走看看