技术交流


SPRINGBOOT之QQ登录

Mar 20, 2019 9:40:40 PM
209
45

最近在网站上做了一个QQ登录OAuth2.0接入的功能,下面我就来分享一下如何实现QQ登录

首先在开发之前我们需要为QQ登录做如下准备:

1、登录QQ互联https://connect.qq.com/成为开发者(成为开发者需要填写资料,审核时间大概2天吧)。

2、有了开发者账号之后就能创建应用,这时会要求你填写网站信息,之后又会进入审核阶段(本人当时几个小时就审核过了)。

接下来就是开发阶段了

根据QQ互联官方网站可以知道QQ登录开发流程如下:

1、获取appidappkeyappidappkey在你创建的应用信息中查找。

2、放置QQ登录按钮。

3、获取Authorization Code

4、通过Authorization Code获取Access Token

5、获取QQ用户的openid

6、通过openid访问/修改QQ用户信息。

以下为开发步骤和示例代码

本项目是一个maven构建的项目。使用fastjson处理json数据,fastjson依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>

一、写好配置功能

1、配置基本信息

## 第三方登录配置信息
oauth:
  qq:
    app_id: ----------------
    app_key: --------------------------------
    # 获取ocde地址
    get_authorization_code_uri: https://graph.qq.com/oauth2.0/authorize
    # 获取ocde的参数,固定为code
    response_type: code
    # 回调地址
    redirect_uri: ---------------------------
    # 获取Access Token地址
    get_access_token_uri: https://graph.qq.com/oauth2.0/token
    # 获取Access Token,固定为authorization_code
    token_grant_type: authorization_code
    # 刷新Access Token,refresh_token
    refresh_token_grant_type: refresh_token
    # 获取openid的地址
    get_openid_uri: https://graph.qq.com/oauth2.0/me
    # 获取用户信息地址
    get_user_info_uri: https://graph.qq.com/user/get_user_info

 

2、编写配置信息的配置类,通过该配置类获取配置信息

@Component
@ConfigurationProperties(prefix = "oauth.qq")
public class QqOAuthConfig {

    private String appId;

    private String appKey;

    /**
     * 获取ocde地址
     */
    private String getAuthorizationCodeUri;

    /**
     * 获取ocde的参数,固定为code
     */
    private String responseType;

    /**
     * 回调地址
     */
    private String redirectUri;

    /**
     * 获取Access Token地址
     */
    private String getAccessTokenUri;

    /**
     * 获取Access Token,固定为authorization_code
     */
    private String tokenGrantType;

    /**
     * 刷新Access Token,refresh_token
     */
    private String refreshTokenGrantType;

    /**
     * 获取openid的地址
     */
    private String getOpenidUri;

    /**
     * 获取用户信息地址
     */
    private String getUserInfoUri;


    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getAppKey() {
        return appKey;
    }

    public void setAppKey(String appKey) {
        this.appKey = appKey;
    }

    public String getGetAuthorizationCodeUri() {
        return getAuthorizationCodeUri;
    }

    public void setGetAuthorizationCodeUri(String getAuthorizationCodeUri) {
        this.getAuthorizationCodeUri = getAuthorizationCodeUri;
    }

    public String getResponseType() {
        return responseType;
    }

    public void setResponseType(String responseType) {
        this.responseType = responseType;
    }

    public String getRedirectUri() {
        return redirectUri;
    }

    public void setRedirectUri(String redirectUri) {
        this.redirectUri = redirectUri;
    }

    public String getGetAccessTokenUri() {
        return getAccessTokenUri;
    }

    public void setGetAccessTokenUri(String getAccessTokenUri) {
        this.getAccessTokenUri = getAccessTokenUri;
    }

    public String getTokenGrantType() {
        return tokenGrantType;
    }

    public void setTokenGrantType(String tokenGrantType) {
        this.tokenGrantType = tokenGrantType;
    }

    public String getRefreshTokenGrantType() {
        return refreshTokenGrantType;
    }

    public void setRefreshTokenGrantType(String refreshTokenGrantType) {
        this.refreshTokenGrantType = refreshTokenGrantType;
    }

    public String getGetOpenidUri() {
        return getOpenidUri;
    }

    public void setGetOpenidUri(String getOpenidUri) {
        this.getOpenidUri = getOpenidUri;
    }

    public String getGetUserInfoUri() {
        return getUserInfoUri;
    }

    public void setGetUserInfoUri(String getUserInfoUri) {
        this.getUserInfoUri = getUserInfoUri;
    }
}

 

二、放置QQ登录按钮

ebd5a312-1155-4105-a673-2464c6e0781e

三、接下来就是QQ登录主要流程。此时我们需要httpclient工具来获取通过请求返回的信息,小伙伴们可以去百度了解一下httpclient

1、加入httpclient依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.7</version>
</dependency>

 

2、配置httpclient工具

1)封装响应结果

public class HttpClientResult implements Serializable {

    /**
     * 响应状态码
     */
    private int code;

    /**
     * 响应数据
     */
    private String content;


    public HttpClientResult() {
    }

    public HttpClientResult(int code) {
        this.code = code;
    }

    public HttpClientResult(int code, String content) {
        this.code = code;
        this.content = content;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

 

2)封装httpClient工具类

public class HttpClientUtil {

    // 编码格式。发送编码格式统一用UTF-8
    private static final String ENCODING = "UTF-8";

    // 设置连接超时时间,单位毫秒。
    private static final int CONNECT_TIMEOUT = 6000;

    // 请求获取数据的超时时间(即响应时间),单位毫秒。
    private static final int SOCKET_TIMEOUT = 6000;

    /**
     * 发送get请求;不带请求头和请求参数
     *
     * @param url 请求地址
     * @return
     * @throws Exception
     */
    public static HttpClientResult doGet(String url) throws Exception {
        return doGet(url, null, null);
    }

    /**
     * 发送get请求;带请求参数
     *
     * @param url    请求地址
     * @param params 请求参数集合
     * @return
     * @throws Exception
     */
    public static HttpClientResult doGet(String url, Map<String, String> params) throws Exception {
        return doGet(url, null, params);
    }

    /**
     * 发送get请求;带请求头和请求参数
     *
     * @param url     请求地址
     * @param headers 请求头集合
     * @param params  请求参数集合
     * @return
     * @throws Exception
     */
    public static HttpClientResult doGet(String url, Map<String, String> headers, Map<String, String> params) throws Exception {
        // 创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建访问的地址
        URIBuilder uriBuilder = new URIBuilder(url);
        if (params != null) {
            Set<Map.Entry<String, String>> entrySet = params.entrySet();
            for (Map.Entry<String, String> entry : entrySet) {
                uriBuilder.setParameter(entry.getKey(), entry.getValue());
            }
        }

        // 创建http对象
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        /**
         * setConnectTimeout:设置连接超时时间,单位毫秒。
         * setConnectionRequestTimeout:设置从connect Manager(连接池)获取Connection
         * 超时时间,单位毫秒。这个属性是新加的属性,因为目前版本是可以共享连接池的。
         * setSocketTimeout:请求获取数据的超时时间(即响应时间),单位毫秒。 如果访问一个接口,多少时间内无法返回数据,就直接放弃此次调用。
         */
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
        httpGet.setConfig(requestConfig);

        // 设置请求头
        packageHeader(headers, httpGet);

        // 创建httpResponse对象
        CloseableHttpResponse httpResponse = null;

        try {
            // 执行请求并获得响应结果
            return getHttpClientResult(httpResponse, httpClient, httpGet);
        } finally {
            // 释放资源
            release(httpResponse, httpClient);
        }
    }

    /**
     * 发送post请求;不带请求头和请求参数
     *
     * @param url 请求地址
     * @return
     * @throws Exception
     */
    public static HttpClientResult doPost(String url) throws Exception {
        return doPost(url, null, null);
    }

    /**
     * 发送post请求;带请求参数
     *
     * @param url    请求地址
     * @param params 参数集合
     * @return
     * @throws Exception
     */
    public static HttpClientResult doPost(String url, Map<String, String> params) throws Exception {
        return doPost(url, null, params);
    }

    /**
     * 发送post请求;带请求头和请求参数
     *
     * @param url     请求地址
     * @param headers 请求头集合
     * @param params  请求参数集合
     * @return
     * @throws Exception
     */
    public static HttpClientResult doPost(String url, Map<String, String> headers, Map<String, String> params) throws Exception {
        // 创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建http对象
        HttpPost httpPost = new HttpPost(url);
        /**
         * setConnectTimeout:设置连接超时时间,单位毫秒。
         * setConnectionRequestTimeout:设置从connect Manager(连接池)获取Connection
         * 超时时间,单位毫秒。这个属性是新加的属性,因为目前版本是可以共享连接池的。
         * setSocketTimeout:请求获取数据的超时时间(即响应时间),单位毫秒。 如果访问一个接口,多少时间内无法返回数据,就直接放弃此次调用。
         */
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
        httpPost.setConfig(requestConfig);
        // 设置请求头
      /*httpPost.setHeader("Cookie", "");
      httpPost.setHeader("Connection", "keep-alive");
      httpPost.setHeader("Accept", "application/json");
      httpPost.setHeader("Accept-Language", "zh-CN,zh;q=0.9");
      httpPost.setHeader("Accept-Encoding", "gzip, deflate, br");
      httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36");*/
        packageHeader(headers, httpPost);

        // 封装请求参数
        packageParam(params, httpPost);

        // 创建httpResponse对象
        CloseableHttpResponse httpResponse = null;

        try {
            // 执行请求并获得响应结果
            return getHttpClientResult(httpResponse, httpClient, httpPost);
        } finally {
            // 释放资源
            release(httpResponse, httpClient);
        }
    }

    /**
     * 发送put请求;不带请求参数
     *
     * @param url 请求地址
     * @param
     * @return
     * @throws Exception
     */
    public static HttpClientResult doPut(String url) throws Exception {
        return doPut(url);
    }

    /**
     * 发送put请求;带请求参数
     *
     * @param url    请求地址
     * @param params 参数集合
     * @return
     * @throws Exception
     */
    public static HttpClientResult doPut(String url, Map<String, String> params) throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPut httpPut = new HttpPut(url);
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
        httpPut.setConfig(requestConfig);

        packageParam(params, httpPut);

        CloseableHttpResponse httpResponse = null;

        try {
            return getHttpClientResult(httpResponse, httpClient, httpPut);
        } finally {
            release(httpResponse, httpClient);
        }
    }

    /**
     * 发送delete请求;不带请求参数
     *
     * @param url 请求地址
     * @param
     * @return
     * @throws Exception
     */
    public static HttpClientResult doDelete(String url) throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpDelete httpDelete = new HttpDelete(url);
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
        httpDelete.setConfig(requestConfig);

        CloseableHttpResponse httpResponse = null;
        try {
            return getHttpClientResult(httpResponse, httpClient, httpDelete);
        } finally {
            release(httpResponse, httpClient);
        }
    }

    /**
     * 发送delete请求;带请求参数
     *
     * @param url    请求地址
     * @param params 参数集合
     * @return
     * @throws Exception
     */
    public static HttpClientResult doDelete(String url, Map<String, String> params) throws Exception {
        if (params == null) {
            params = new HashMap<String, String>();
        }

        params.put("_method", "delete");
        return doPost(url, params);
    }

    /**
     * Description: 封装请求头
     *
     * @param params
     * @param httpMethod
     */
    public static void packageHeader(Map<String, String> params, HttpRequestBase httpMethod) {
        // 封装请求头
        if (params != null) {
            Set<Map.Entry<String, String>> entrySet = params.entrySet();
            for (Map.Entry<String, String> entry : entrySet) {
                // 设置到请求头到HttpRequestBase对象中
                httpMethod.setHeader(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * Description: 封装请求参数
     *
     * @param params
     * @param httpMethod
     * @throws UnsupportedEncodingException
     */
    public static void packageParam(Map<String, String> params, HttpEntityEnclosingRequestBase httpMethod)
            throws UnsupportedEncodingException {
        // 封装请求参数
        if (params != null) {
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            Set<Map.Entry<String, String>> entrySet = params.entrySet();
            for (Map.Entry<String, String> entry : entrySet) {
                nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }

            // 设置到请求的http对象中
            httpMethod.setEntity(new UrlEncodedFormEntity(nvps, ENCODING));
        }
    }

    /**
     * Description: 获得响应结果
     *
     * @param httpResponse
     * @param httpClient
     * @param httpMethod
     * @return
     * @throws Exception
     */
    public static HttpClientResult getHttpClientResult(CloseableHttpResponse httpResponse,CloseableHttpClient httpClient, HttpRequestBase httpMethod) throws Exception {
        // 执行请求
        httpResponse = httpClient.execute(httpMethod);

        // 获取返回结果
        if (httpResponse != null && httpResponse.getStatusLine() != null) {
            String content = "";
            if (httpResponse.getEntity() != null) {
                content = EntityUtils.toString(httpResponse.getEntity(), ENCODING);
            }
            return new HttpClientResult(httpResponse.getStatusLine().getStatusCode(), content);
        }
        return new HttpClientResult(HttpStatus.SC_INTERNAL_SERVER_ERROR);
    }

    /**
     * Description: 释放资源
     *
     * @param httpResponse
     * @param httpClient
     * @throws IOException
     */
    public static void release(CloseableHttpResponse httpResponse, CloseableHttpClient httpClient) throws IOException {
        // 释放资源
        if (httpResponse != null) {
            httpResponse.close();
        }
        if (httpClient != null) {
            httpClient.close();
        }
    }
}

 

3、点击QQ登录的按钮获取会跳转到QQ登录页面。如果QQ授权登录成功将会跳转到指定的回调地址(创建应用是填写的地址),获取到Authorization Code,该code有效期10分钟。

7bb0f48b-ab3a-40ad-93d4-8effa8f8a7cd

@GetMapping("/login/qq")
public void loginQQ(HttpServletResponse httpServletResponse, String thirdLoginBackHref) {
    try {
        httpServletResponse.sendRedirect(qqOAuthConfig.getGetAuthorizationCodeUri() +
                "?response_type=" + qqOAuthConfig.getResponseType() +
                "&client_id=" + qqOAuthConfig.getAppId() +
                "&redirect_uri=" + qqOAuthConfig.getRedirectUri() +
                "&state=" + UuidUtil.getUuid());
    } catch (Exception e) {
        throw new BaseException("请求参数异常");
    }
}

 

1、编写回调地址。通过Authorization Code获取Access Token2小时有效期);获取QQ用户的openid;通过openid访问QQ用户信息。

1)回调controller

@GetMapping("/login/qq/callback")
public String qqCallbackUri(String code) {
    loginService.qqLogin(code);
    return "/"; //返回地址
}

 

2)回调service,获取用户信息。

@Override
@Transactional
public void qqLogin(String code) {
    //获取access_token的请求参数
    HashMap<String, String> getTokenParams = new HashMap<>();
    getTokenParams.put("grant_type", qqOAuthConfig.getTokenGrantType());
    getTokenParams.put("client_id", qqOAuthConfig.getAppId());
    getTokenParams.put("client_secret", qqOAuthConfig.getAppKey());
    getTokenParams.put("code", code);
    getTokenParams.put("redirect_uri", qqOAuthConfig.getRedirectUri());
    //获取access_token
    HttpClientResult resultOfGetAcccessToken = null;
    try {
        resultOfGetAcccessToken = HttpClientUtil.doGet(qqOAuthConfig.getGetAccessTokenUri(), getTokenParams);
    } catch (Exception e) {
        throw new BaseException("QQ登录异常");
    }
    //获取access_token返回参数
    String resultParamsOfGetToken = resultOfGetAcccessToken.getContent();
    if (resultOfGetAcccessToken.getCode() != 200 || (resultParamsOfGetToken == null || resultParamsOfGetToken.equals(""))) {
        throw new BaseException("QQ登录异常");
    }
    //处理获取access_token返回参数成json
    String[] tokenResultString = resultParamsOfGetToken.split("&");
    Map<String, String> tokenResultMap = new HashMap<>();
    for (String s : tokenResultString) {
        String keyValue[] = s.split("=");
        if (keyValue.length > 1) {
            tokenResultMap.put(keyValue[0], keyValue[1]);
        }
    }
    //access_token
    String accessToken = tokenResultMap.get("access_token");
    //获取openid
    HttpClientResult resultOfGetOpenid = null;
    try {
        resultOfGetOpenid = HttpClientUtil.doGet(qqOAuthConfig.getGetOpenidUri() + "?access_token=" + accessToken);
    } catch (Exception e) {
        throw new BaseException("QQ登录异常");
    }
    //处理获取openid结果
    String resultOfOpenidContent = resultOfGetOpenid.getContent();
    if (resultOfGetOpenid.getCode() != 200 || (resultOfOpenidContent == null || resultOfOpenidContent.equals(""))) {
        throw new BaseException("QQ登录异常");
    }
    JSONObject resultByGetOpenIdJson = JSON.parseObject(resultOfOpenidContent.replaceAll("callback\\( | \\);", "").trim());
    //openid
    String openid = (String) resultByGetOpenIdJson.get("openid");
    if (resultByGetOpenIdJson.get("client_id") == null || openid == null) {
        throw new BaseException("QQ登录异常");
    }
    //获取用户信息
    HttpClientResult resultByGetUserInfo = null;
    try {
        resultByGetUserInfo = HttpClientUtil.doGet(qqOAuthConfig.getGetUserInfoUri() +
                "?access_token=" + accessToken +
                "&oauth_consumer_key=" + qqOAuthConfig.getAppId() +
                "&openid=" + openid);
    } catch (Exception e) {
        throw new BaseException("QQ登录异常");
    }
    //处理用户数据
    JSONObject qqUserInfoJson = JSON.parseObject(resultByGetUserInfo.getContent());
    //qq头像
    String chatHead = (String) qqUserInfoJson.get("figureurl_qq_1");
    //qq昵称
    String nickname = (String) qqUserInfoJson.get("nickname");
    //以下便是登录逻辑,这里就省略不多写了,大家可以根据自己的需求编写登录逻辑。
}

 

说明

上面代码最后的变量qqUserInfoJson便是一个包含了QQ用户信息的json对象,我们可以将自己需要的用户信息取出来,必要时保存在数据库。

 

虽然省略的登录部分的代码,但在这里也简要说一下QQ登录逻辑:首先QQ登录对于自己的项目肯定是免密登录;其次openid是每个QQ用户在本网站的唯一标识,所以我们QQ登录的用户表一般会存每个用户的openid,在获取到QQ用户信息时判断数据库是否存在与当前openid相同的用户数据,如果没有便添加一条与当前openid相同的用户信息然后登录,如果存在,可以选择直接进入登录逻辑或修改相关登录信息后进入登录逻辑。

 

这里只介绍简单的QQ登录流程,并没有持续去请求腾讯API获取相关信息,所以刷新access_token功能以后再加入。

 

至此QQ登录就分享完了,感谢大家的支持!


如果你喜欢我的内容,就请打赏一下吧
微信
支付宝
温馨提示: 你的打赏金额会直接转入对方账户,不可退回。

评论专区


审核通过的评论(1)
admn
Mar 21, 2019 9:30:34 AM

666

个人名片

  欢迎来到“浩瀚星尘”的个人博客!
  首先,该博客用于分享本人的生活事迹与兴趣爱好; 此外,该博客的主要作用便是与广大的小伙伴一起分享探讨开发技术, 希望大家多多关照。

网名: 浩瀚星尘
城市: 重庆
工作: java