准备工作

1.云服务器
2.备案的域名
3.本地调试需要修改hosts文件,将域名映射到127.0.0.1

一、申请QQ互联,并成为开发者

QQ互联:https://connect.qq.com/index.html

1.1 登录后,点击头像,进入认证页面,填写信息,等待审核。

image.png

1.2 审核通过后,点击创建应用

Java实现第三方QQ登录 - 图2

Java实现第三方QQ登录 - 图3

Java实现第三方QQ登录 - 图4

1.3 审核通过后,就可以使用APP ID 和 APP Key

Java实现第三方QQ登录 - 图5

二、网站开发的流程

2.1 准备工作_OAuth2.0

本步骤的作用

接入QQ登录前,网站需首先进行申请,获得对应的appid与appkey,以保证后续流程中可正确对网站与用户进行验证与授权。

本步骤在整个流程中的位置
Java实现第三方QQ登录 - 图6
开发者文档

https://wiki.connect.qq.com/%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c_oauth2-0
image.png

2.2 放置“QQ登录”按钮_OAuth2.0

2.2.1 下载“QQ登录”按钮图片,并将按钮放置在页面合适的位置

按钮图片下载: 点击这里下载
image.png
按照UI规范,将按钮放置在页面合适的位置:点击这里查看

2.2.2 为“QQ登录”按钮添加前台代码

2.2.2.1 效果演示

用户在页面上点击“QQ登录”按钮,将触发QQ登录对话框,效果如下图所示:

Java实现第三方QQ登录 - 图9

2.2.2.2 前台代码

为了实现上述效果,应该为“QQ登录”按钮图片添加如下前台代码:

  1. <img src=QQ登录图标文件在服务器上的地址 onclick=按钮点击事件>

2.2.2.3 代码示例

1)写一个函数“toLogin()”,该函数通过调用“index.php”中的qq_login函数来实现将页面跳转到QQ登录页面。

(示例中的oauth/index.php,请参见从SDK下载页面下载PHP SDK,在Connect2.1文件夹下的index.php文件。)

<script>
 function toLogin()
 {
   //以下为按钮点击事件的逻辑。注意这里要重新打开窗口
   //否则后面跳转到QQ登录,授权页面时会直接缩小当前浏览器的窗口,而不是打开新窗口
   var A=window.open("oauth/index.php","TencentLogin",
   "width=450,height=320,menubar=0,scrollbars=1,
   resizable=1,status=1,titlebar=0,toolbar=0,location=1");
 }
</script>

2)为按钮添加“toLogin()”事件:

<a href="#" onclick='toLogin()'>
 <img src="img/qq_login.png"></a>

3.3 使用Authorization_Code获取Access_Token

3.3.1 导入POM依赖

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.2</version>
</dependency>
<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpcore</artifactId>
  <version>4.4.10</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.47</version>
</dependency>

3.3.2 抽取QQutils工具类,用来封装APP_ID、APP_KEY等常量,以及获取token和openId及根据openId获取用户信息的工具方法

package com.gmw.boot.send_meail.utils;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;

/**
 * 这是一个QQ的工具类,获取token,openID以及用户的信息
 */
public class QQutils {
    //设置用户的APP_ID
    public static final String APP_ID = "101870215";
    //设置用户的APP_KEY
    public static final String APP_KEY = "5fdd507e863617d7f33b1d69ed5a1eed";
    //设置用户的回调地址
    public static final String REDIRECT_URL = "http://www.raincoding.cn/qq/callback";

    /**
     * 获取Token的方法
     */
    public static String getAccessToken(String url) throws IOException {
        String token = null;
        //创建client对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet(url);
        //执行execute方法,返回response
        HttpResponse response = client.execute(httpGet);
        //获取响应体
        HttpEntity entity = response.getEntity();

        //判断响应体是否为空
        if(entity != null){
            String result = EntityUtils.toString(entity, "UTF-8");
            //返回一串json字符串
            //access_token=SHDJS45645FAD4ASD45
            //expires_in=77766000
            //refresh_token=DJFHDJSF5445SDSD

            String[] all = result.split("&");
            for (String s : all) {
                if(s.indexOf("=") > 0){
                    token = s.substring(s.indexOf("=") + 1);
                    break;
                }
            }

            System.out.println(result);
        }
        return token;
    }

    /**
     * 根据token,得到对应用户身份的OpenID。
     */
    public static String getOpenId(String url) throws IOException{
        String openId = null;
        //创建client对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet(url);
        //执行execute方法,返回response
        HttpResponse response = client.execute(httpGet);
        //获取响应体
        HttpEntity entity = response.getEntity();

        //判断响应体是否为空
        if(entity != null){
            String result = EntityUtils.toString(entity, "UTF-8");
            //返回一串json字符串
            //({"cliend_id":"1056365","openId":"4687FASDAS61ER"})

            //调用工具类,将json字符串转换成json对象
            JSONObject ob = JSONUtils.castToJson(result);

            openId = ob.getString("openid");

            System.out.println(result);
        }
        return openId;
    }

    /**
     * 根据openId获取用户的信息,因为openId是唯一的
     */
    public static JSONObject getUserInfo(String url) throws IOException{
        JSONObject object = null;
        //创建client对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet(url);
        //执行execute方法,返回response
        HttpResponse response = client.execute(httpGet);
        //获取响应体
        HttpEntity entity = response.getEntity();

        //判断响应体是否为空
        if(entity != null){
            String result = EntityUtils.toString(entity, "UTF-8");

            object = JSONObject.parseObject(result);

            System.out.println(result);
        }
        return object;
    }

}

3.3.3 创建JSONUtils工具类用来将json字符串转换成json对象

package com.gmw.boot.send_meail.utils;

import com.alibaba.fastjson.JSONObject;

public class JSONUtils {
    public static JSONObject castToJson(String jsoup){
        int startIndex = jsoup.indexOf("(");
        int endIndex = jsoup.indexOf(")");
        String json = jsoup.substring(startIndex + 1,endIndex);
        JSONObject object = JSONObject.parseObject(json);
        return object;
    }
}

3.3.4 获取Authorization Code

请求地址

PC网站:https://graph.qq.com/oauth2.0/authorize

请求方法

GET

请求参数

请求参数请包含如下内容:

参数 是否必须 含义
response_type 必须 授权类型,此值固定为“code”。
client_id 必须 申请QQ登录成功后,分配给应用的appid。
redirect_uri 必须 成功授权后的回调地址,必须是注册appid时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。注意需要将url进行URLEncode。
state 必须 client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。
scope 可选 请求用户授权时向用户显示的可进行授权的列表。
可填写的值是API文档中列出的接口,如果要填写多个接口名称,请用逗号隔开。
例如:scope=get_user_info,list_album,upload_pic
不传则默认请求对接口get_user_info进行授权。
建议控制授权项的数量,只传入必要的接口名称,因为授权项越多,用户越可能拒绝进行任何授权。
display 可选 PC网站接入时使用。
用于展示的样式。不传则默认展示为PC下的样式。
如果传入“mobile”,则展示为mobile端下的样式。
<a th:href="@{/qq/auth}"><img th:src="@{/image/qq.png}"/></a>

image.png

    @RequestMapping(value = "/qq/auth")
    public String oauth(HttpSession session){
        //生成UUID当作唯一的值,传递给回调地址当作state,用于第三方应用防止CSRF攻击
        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        //将生成的UUID放到session中
        session.setAttribute("state",uuid);
        //发送请求,获取获取Authorization Code
        String url = "https://graph.qq.com/oauth2.0/authorize?response_type=code" +
                "&client_id=" + QQutils.APP_ID +
                "&redirect_uri=" + QQutils.REDIRECT_URL +
                "&state=" + uuid;

        return "redirect:" + url;
    }

返回说明

  1. 如果用户成功登录并授权,则会跳转到指定的回调地址,并在redirect_uri地址后带上Authorization Code和原始的state值。如:

PC网站:http://graph.qq.com/demo/index.jsp?code=9A5F**06AF&state=test

注意:此code会在10分钟内过期。

  1. 如果用户在登录授权过程中取消登录流程,对于PC网站,登录页面直接关闭;

错误码说明

接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。

PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码

3.3.5 通过Authorization Code获取Access Token

请求地址

PC网站:https://graph.qq.com/oauth2.0/token

请求方法

GET

请求参数

请求参数请包含如下内容:

参数 是否必须 含义
grant_type 必须 授权类型,在本步骤中,此值为“authorization_code”。
client_id 必须 申请QQ登录成功后,分配给网站的appid。
client_secret 必须 申请QQ登录成功后,分配给网站的appkey。
code 必须 上一步返回的authorization code。
如果用户成功登录并授权,则会跳转到指定的回调地址,并在URL中带上Authorization Code。
例如,回调地址为www.qq.com/my.php,则跳转到:
http://www.qq.com/my.php?code=520DD95263C1CFEA087**
注意此code会在10分钟内过期。
redirect_uri 必须 与上面一步中传入的redirect_uri保持一致。
fmt 可选 因历史原因,默认是x-www-form-urlencoded格式,如果填写json,则返回json格式
  /**
     * 如果用户成功登录并授权,则会跳转到指定的回调地址,
     * 并在redirect_uri地址后带上Authorization Code和原始的state值。
     * @return
     */
    @RequestMapping(value = "/qq/callback")
    public String callback(HttpServletRequest request, HttpServletResponse response,Model model){
        //创建HttpSession对象
        HttpSession session = request.getSession();
        //获取session中的code,也就是存入的uuid
        String uuid = (String) session.getAttribute("state");

        //获取code参数
        String code = request.getParameter("code");
        //获取state参数
        String state = request.getParameter("state");

        //判单获取的state参数和session中uuid值是否一样
        if(state != null){
            if(!uuid.equals(state)){
                throw new RuntimeException("Q获取的state参数和session中uuid值不一样");
            }
        }

        //通过Authorization Code获取Access Token
        String url = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code" +
                "&client_id=" + QQutils.APP_ID +
                "&client_secret=" + QQutils.APP_KEY +
                "&code=" + code +
                "&redirect_uri=" + QQutils.REDIRECT_URL;

        //获取token
        String token = null;

        try {
            QQutils.getAccessToken(url);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("token = " + token);

        TODO 后面还有代码...

    }

调用getAccessToken(url)方法返回token

/**
     * 获取Token的方法
     */
    public static String getAccessToken(String url) throws IOException {
        String token = null;
        //创建client对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet(url);
        //执行execute方法,返回response
        HttpResponse response = client.execute(httpGet);
        //获取响应体
        HttpEntity entity = response.getEntity();

        //判断响应体是否为空
        if(entity != null){
            String result = EntityUtils.toString(entity, "UTF-8");
            //返回一串json字符串
            //access_token=SHDJS45645FAD4ASD45
            //expires_in=77766000
            //refresh_token=DJFHDJSF5445SDSD

            String[] all = result.split("&");
            for (String s : all) {
                if(s.indexOf("=") > 0){
                    token = s.substring(s.indexOf("=") + 1);
                    break;
                }
            }

            System.out.println(result);
        }
        return token;
    }

返回说明

如果成功返回,即可在返回包中获取到Access Token。 如(不指定fmt时):

access_token=FE04**CCE2&expires_in=7776000&refresh_token=88E4**BE14

参数说明 描述
access_token 授权令牌,Access_Token。
expires_in 该access token的有效期,单位为秒。
refresh_token 在授权自动续期步骤中,获取新的Access_Token时需要提供的参数。注:refresh_token仅一次有效

错误码说明

接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。

PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码

3.4 获取用户OpenID_OAuth2.0

请求地址
**
PC网站:https://graph.qq.com/oauth2.0/me

请求方法
**
GET

请求参数
**
请求参数请包含如下内容:

参数 是否必须 含义
access_token 必须 在Step1中获取到的access token。
fmt 可选 因历史原因,默认是jsonpb格式,如果填写json,则返回json格式
 /**
     * 如果用户成功登录并授权,则会跳转到指定的回调地址,
     * 并在redirect_uri地址后带上Authorization Code和原始的state值。
     * @return
     */
    @RequestMapping(value = "/qq/callback")
    public String callback(HttpServletRequest request, HttpServletResponse response,Model model){
        //创建HttpSession对象
        HttpSession session = request.getSession();
        //获取session中的code,也就是存入的uuid
        String uuid = (String) session.getAttribute("state");

        //获取code参数
        String code = request.getParameter("code");
        //获取state参数
        String state = request.getParameter("state");

        //判单获取的state参数和session中uuid值是否一样
        if(state != null){
            if(!uuid.equals(state)){
                throw new RuntimeException("Q获取的state参数和session中uuid值不一样");
            }
        }

        //通过Authorization Code获取Access Token
        String url = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code" +
                "&client_id=" + QQutils.APP_ID +
                "&client_secret=" + QQutils.APP_KEY +
                "&code=" + code +
                "&redirect_uri=" + QQutils.REDIRECT_URL;

        //获取token
        String token = null;

        try {
            QQutils.getAccessToken(url);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("token = " + token);


        //获取用户OpenID_OAuth2.0
        url = "https://graph.qq.com/oauth2.0/me?access_token=" + token;

        //获取openId
        String openId = null;

        try {
            openId = QQutils.getOpenId(url);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("openId = " + openId);

        TODO 后面还有代码...

}

调用getOpenId(url)返回openId_

 /**
     * 根据token,得到对应用户身份的OpenID。
     */
    public static String getOpenId(String url) throws IOException{
        String openId = null;
        //创建client对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet(url);
        //执行execute方法,返回response
        HttpResponse response = client.execute(httpGet);
        //获取响应体
        HttpEntity entity = response.getEntity();

        //判断响应体是否为空
        if(entity != null){
            String result = EntityUtils.toString(entity, "UTF-8");
            //返回一串json字符串
            //({"cliend_id":"1056365","openId":"4687FASDAS61ER"})

            //调用工具类,将json字符串转换成json对象
            JSONObject ob = JSONUtils.castToJson(result);

            openId = ob.getString("openid");

            System.out.println(result);
        }
        return openId;
    }

返回说明
**
PC网站接入时,获取到用户OpenID,返回包如下(如果fmt参数未指定):

callback( {“client_id”:”YOUR_APPID”,”openid”:”YOUR_OPENID”} );

openid是此网站上唯一对应用户身份的标识,网站可将此ID进行存储便于用户下次登录时辨识其身份,或将其与用户在网站上的原有账号进行绑定。

错误码说明

接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。

PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码

3.5 根据openId获取用户的信息

_

前提说明

1. 该appid已经开通了该OpenAPI的使用权限。
**
API列表的接口列表中可以看到,有的接口是完全开放的,有的接口则需要提前提交申请,以获取访问权限。

2. 准备访问的资源是用户授权可访问的。
**
网站调用该OpenAPI读写某个openid(用户)的信息时,必须是该用户已经对你的appid进行了该OpenAPI的授权(例如用户已经设置了相册不对外公开,则网站是无法读取照片信息的)。
用户可以进入手机QQ->设置->隐私->授权管理,进行访问权限管理。

3. 已经成功获取到Access Token,并且Access Token在有效期内。
**

调用OpenAPI接口

QQ登录提供了用户信息等OpenAPI(详见API列表),网站需要将请求发送到某个具体的OpenAPI接口,以访问或修改用户数据。

调用所有OpenAPI时,除了各接口私有的参数外,所有OpenAPI都需要传入基于OAuth2.0协议的通用参数:
**

参数 含义
access_token 可通过使用Authorization_Code获取Access_Token 或来获取。
access_token有3个月有效期。
oauth_consumer_key 申请QQ登录成功后,分配给应用的appid
openid 用户的ID,与QQ号码一一对应。
可通过调用https://graph.qq.com/oauth2.0/me?access_token=YOUR_ACCESS_TOKEN 来获取。

示例

1. 以get_user_info接口为例:

(请将access_token,appid等参数值替换为你自己的)

https://graph.qq.com/user/get_user_info?access_token=YOUR_ACCESS_TOKEN&oauth_consumer_key=YOUR_APP_ID&openid=YOUR_OPENID

2. 成功返回后,即可获取到用户数据:

{
   "ret":0,
   "msg":"",
   "nickname":"YOUR_NICK_NAME",
  ...
}

3.下一步
**
获取用户的Access Token和OpenID,之后通过调用OpenAPI进行获取用户信息等操作。

/**
     * 如果用户成功登录并授权,则会跳转到指定的回调地址,
     * 并在redirect_uri地址后带上Authorization Code和原始的state值。
     * @return
     */
    @RequestMapping(value = "/qq/callback")
    public String callback(HttpServletRequest request, HttpServletResponse response,Model model){
        //创建HttpSession对象
        HttpSession session = request.getSession();
        //获取session中的code,也就是存入的uuid
        String uuid = (String) session.getAttribute("state");

        //获取code参数
        String code = request.getParameter("code");
        //获取state参数
        String state = request.getParameter("state");

        //判单获取的state参数和session中uuid值是否一样
        if(state != null){
            if(!uuid.equals(state)){
                throw new RuntimeException("Q获取的state参数和session中uuid值不一样");
            }
        }

        //通过Authorization Code获取Access Token
        String url = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code" +
                "&client_id=" + QQutils.APP_ID +
                "&client_secret=" + QQutils.APP_KEY +
                "&code=" + code +
                "&redirect_uri=" + QQutils.REDIRECT_URL;

        //获取token
        String token = null;

        try {
            QQutils.getAccessToken(url);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("token = " + token);


        //获取用户OpenID_OAuth2.0
        url = "https://graph.qq.com/oauth2.0/me?access_token=" + token;

        //获取openId
        String openId = null;

        try {
            openId = QQutils.getOpenId(url);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("openId = " + openId);

        //根据openId获取用户的信息,因为openId是唯一的
        url = "https://graph.qq.com/user/get_user_info?access_token=" + token +
        "&oauth_consumer_key=" + QQutils.APP_ID +
        "&openid=" + openId;

        JSONObject object = null;
        try {
            object = QQutils.getUserInfo(url);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("object = " + object);

        //nickname 用户的昵称



        String nickname = object.getString("nickname");

        session.setAttribute("loginUser",nickname);

        session.setAttribute("time",new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return "main";
    }

调用getUserInfo(url)返回用户的信息

 /**
     * 根据openId获取用户的信息,因为openId是唯一的
     */
    public static JSONObject getUserInfo(String url) throws IOException{
        JSONObject object = null;
        //创建client对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建HttpGet请求
        HttpGet httpGet = new HttpGet(url);
        //执行execute方法,返回response
        HttpResponse response = client.execute(httpGet);
        //获取响应体
        HttpEntity entity = response.getEntity();

        //判断响应体是否为空
        if(entity != null){
            String result = EntityUtils.toString(entity, "UTF-8");

            object = JSONObject.parseObject(result);

            System.out.println(result);
        }
        return object;
    }

3.6 测试

image.png

3.7 码云地址:https://gitee.com/Program_Monkey/send-email.git

image.png