从零玩转第三方登录之QQ登录

从零玩转第三方登录之QQ登录前言在真正开始对接之前,我们先来聊一聊后台的方案设计。既然是对接第三方登录,那就免不了如何将用户信息保存。首先需要明确一点的是,用户在第三方登录成功之后,

我们能拿到的仅仅是一个代表用户唯一身份的ID(微博是真实uid,QQ是加密的openID)以及用来识别身份的accessToken,当然还有昵称、头像、性别等有限资料,

对接第三方登录的关键就是如何确定用户是合法登录,如果确定这次登录的和上次登录的是同一个人并且不是假冒的。其实这个并不用我们特别操心,就以微博登录为例,

用户登录成功之后会回调一个code给我们,然后我们再拿code去微博那换取 accessToken ,如果这个code是用户乱填的,那这一关肯定过不了,所以,前面的担心有点多余,哈哈。

准备工作1、前往腾讯云注册域名

2、购买腾讯云服务器用于qq登录

1. 认识Oauth2.0现在很多网站都要不管是为了引流也好,为了用户方便也好一般都有第三方账号登陆的需求,今天以QQ登陆为例,来实现一个最简单的第三方登陆。

目前主流的第三方登录都是依赖的Oauth2.0实现的,最常见的就是在各种中小型网站或者App中的QQ登录,微信登录等等。所以我建议想要学习和实现第三方登录同学去了解下这个协议。

必须要域名并且进行备案比如我的域名: https://yangbuyi.top/

因为腾讯有一个域名认证机制啥的。。。。。。

2.实名认证

QQ登录我们对接的是QQ互联,地址:https://connect.qq.com ,首先需要注册成为开发者并实名认证,需要手持身份证照片,具体就不讲了。

2.1、进行申请开发者身份image2.2 创建应用进入应用管理页面创建应用,根据实际需要是创建网站应用还是移动应用,我这里是网站应用:imageimage提交成功完步后等待客服审核即可image这是我网站的基本接口信息imageQQ登陆流程image请求参数image创建springboot工程image依赖代码语言:java复制

org.apache.httpcomponents

httpcore

4.4.11

org.apache.httpcomponents

httpclient

4.5.8

org.apache.httpcomponents

httpasyncclient

org.apache.httpcomponents

httpmime

com.google.code.gson

gson

2.8.5

net.gplatform

Sdk4J

2.0

com.alibaba

fastjson

1.2.62

org.springframework.boot

spring-boot-starter-thymeleaf

org.springframework.boot

spring-boot-starter-web

org.projectlombok

lombok

true

org.springframework.boot

spring-boot-configuration-processor

true

创建http请求工具代码语言:java复制import com.alibaba.fastjson.JSONObject;

import org.apache.http.HttpEntity;

import org.apache.http.HttpResponse;

import org.apache.http.NameValuePair;

import org.apache.http.client.HttpClient;

import org.apache.http.client.config.RequestConfig;

import org.apache.http.client.entity.UrlEncodedFormEntity;

import org.apache.http.client.methods.CloseableHttpResponse;

import org.apache.http.client.methods.HttpGet;

import org.apache.http.client.methods.HttpPost;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;

import org.apache.http.conn.ssl.TrustStrategy;

import org.apache.http.entity.StringEntity;

import org.apache.http.impl.client.CloseableHttpClient;

import org.apache.http.impl.client.HttpClients;

import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;

import org.apache.http.message.BasicNameValuePair;

import org.apache.http.ssl.SSLContextBuilder;

import org.apache.http.util.EntityUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import javax.net.ssl.HostnameVerifier;

import javax.net.ssl.SSLContext;

import javax.net.ssl.SSLSession;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.nio.charset.Charset;

import java.security.GeneralSecurityException;

import java.security.cert.CertificateException;

import java.security.cert.X509Certificate;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.stream.Collectors;

/**

* description: 杨不易网站 :www.yangbuyi.top

* ClassName: HttpsUtils

* create: 2020-06-24 17:30

*

* @author: yangbuyi

* @since: JDK1.8

**/

public class HttpsUtils {

private static PoolingHttpClientConnectionManager connMgr;

private static RequestConfig requestConfig;

private static final int MAX_TIMEOUT = 7000;

private static final Logger logger = LoggerFactory.getLogger(HttpsUtils.class);

static {

// 设置连接池

connMgr = new PoolingHttpClientConnectionManager();

// 设置连接池大小

connMgr.setMaxTotal(100);

connMgr.setDefaultMaxPerRoute(connMgr.getMaxTotal());

// Validate connections after 1 sec of inactivity

connMgr.setValidateAfterInactivity(1000);

RequestConfig.Builder configBuilder = RequestConfig.custom();

// 设置连接超时

configBuilder.setConnectTimeout(MAX_TIMEOUT);

// 设置读取超时

configBuilder.setSocketTimeout(MAX_TIMEOUT);

// 设置从连接池获取连接实例的超时

configBuilder.setConnectionRequestTimeout(MAX_TIMEOUT);

requestConfig = configBuilder.build();

}

/**

* 发送 GET 请求(HTTP),不带输入数据

*

* @param url

* @return

*/

public static String doGet(String url) {

return doGet(url, new HashMap());

}

/**

* 发送 GET 请求(HTTP),K-V形式

*

* @param url

* @param params

* @return

*/

public static String doGet(String url, Map params) {

String apiUrl = url;

StringBuffer param = new StringBuffer();

int i = 0;

for (String key : params.keySet()) {

if (i == 0)

param.append("?");

else

param.append("&");

param.append(key).append("=").append(params.get(key));

i++;

}

apiUrl += param;

String result = null;

HttpClient httpClient = null;

if (apiUrl.startsWith("https")) {

httpClient = HttpClients.custom().setSSLSocketFactory(createSSLConnSocketFactory())

.setConnectionManager(connMgr).setDefaultRequestConfig(requestConfig).build();

} else {

httpClient = HttpClients.createDefault();

}

try {

HttpGet httpGet = new HttpGet(apiUrl);

HttpResponse response = httpClient.execute(httpGet);

HttpEntity entity = response.getEntity();

if (entity != null) {

InputStream instream = entity.getContent();

result = new BufferedReader(new InputStreamReader(instream)).lines().collect(Collectors.joining(System.lineSeparator()));

}

} catch (IOException e) {

logger.error(e.getMessage());

}

return result;

}

/**

* 发送 POST 请求(HTTP),不带输入数据

*

* @param apiUrl

* @return

*/

public static String doPost(String apiUrl) {

return doPost(apiUrl, new HashMap());

}

/**

* 发送 POST 请求,K-V形式

*

* @param apiUrl API接口URL

* @param params 参数map

* @return

*/

public static String doPost(String apiUrl, Map params) {

CloseableHttpClient httpClient = null;

if (apiUrl.startsWith("https")) {

httpClient = HttpClients.custom().setSSLSocketFactory(createSSLConnSocketFactory())

.setConnectionManager(connMgr).setDefaultRequestConfig(requestConfig).build();

} else {

httpClient = HttpClients.createDefault();

}

String httpStr = null;

HttpPost httpPost = new HttpPost(apiUrl);

CloseableHttpResponse response = null;

try {

httpPost.setConfig(requestConfig);

List pairList = new ArrayList<>(params.size());

for (Map.Entry entry : params.entrySet()) {

NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue().toString());

pairList.add(pair);

}

httpPost.setEntity(new UrlEncodedFormEntity(pairList, Charset.forName("UTF-8")));

response = httpClient.execute(httpPost);

HttpEntity entity = response.getEntity();

httpStr = EntityUtils.toString(entity, "UTF-8");

} catch (IOException e) {

logger.error(e.getMessage());

} finally {

if (response != null) {

try {

EntityUtils.consume(response.getEntity());

} catch (IOException e) {

logger.error(e.getMessage());

}

}

}

return httpStr;

}

/**

* 发送 POST 请求,JSON形式

*

* @param apiUrl

* @param json json对象

* @return

*/

public static String doPost(String apiUrl, Object json) {

CloseableHttpClient httpClient = null;

if (apiUrl.startsWith("https")) {

httpClient = HttpClients.custom().setSSLSocketFactory(createSSLConnSocketFactory())

.setConnectionManager(connMgr).setDefaultRequestConfig(requestConfig).build();

} else {

httpClient = HttpClients.createDefault();

}

String httpStr = null;

HttpPost httpPost = new HttpPost(apiUrl);

CloseableHttpResponse response = null;

try {

httpPost.setConfig(requestConfig);

StringEntity stringEntity = new StringEntity(json.toString(), "UTF-8");// 解决中文乱码问题

stringEntity.setContentEncoding("UTF-8");

stringEntity.setContentType("application/json");

httpPost.setEntity(stringEntity);

response = httpClient.execute(httpPost);

HttpEntity entity = response.getEntity();

httpStr = EntityUtils.toString(entity, "UTF-8");

} catch (IOException e) {

logger.error(e.getMessage());

} finally {

if (response != null) {

try {

EntityUtils.consume(response.getEntity());

} catch (IOException e) {

logger.error(e.getMessage());

}

}

}

return httpStr;

}

/**

* 创建SSL安全连接

*

* @return

*/

private static SSLConnectionSocketFactory createSSLConnSocketFactory() {

SSLConnectionSocketFactory sslsf = null;

try {

SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {

@Override

public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {

return true;

}

}).build();

sslsf = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {

@Override

public boolean verify(String arg0, SSLSession arg1) {

return true;

}

});

} catch (GeneralSecurityException e) {

logger.error(e.getMessage());

}

return sslsf;

}

/*gitHub开始*/

/**

* 发送get请求,利用java代码发送请求

* @param url

* @return

* @throws Exception

*/

public static String doGetHub(String url) throws Exception{

CloseableHttpClient httpclient = HttpClients.createDefault();

HttpGet httpGet = new HttpGet(url);

// 发送了一个http请求

CloseableHttpResponse response = httpclient.execute(httpGet);

// 如果响应200成功,解析响应结果

if(response.getStatusLine().getStatusCode()==200){

// 获取响应的内容

HttpEntity responseEntity = response.getEntity();

return EntityUtils.toString(responseEntity);

}

return null;

}

/**

* 将字符串转换成map

* @param responseEntity

* @return

*/

public static Map getMap(String responseEntity) {

Map map = new HashMap<>();

// 以&来解析字符串

String[] result = responseEntity.split("\\&");

for (String str : result) {

// 以=来解析字符串

String[] split = str.split("=");

// 将字符串存入map中

if (split.length == 1) {

map.put(split[0], null);

} else {

map.put(split[0], split[1]);

}

}

return map;

}

/**

* 通过json获得map

* @param responseEntity

* @return

*/

public static Map getMapByJson(String responseEntity) {

Map map = new HashMap<>();

// 阿里巴巴fastjson 将json转换成map

JSONObject jsonObject = JSONObject.parseObject(responseEntity);

for (Map.Entry entry : jsonObject.entrySet()) {

String key = entry.getKey();

// 将obj转换成string

String value = String.valueOf(entry.getValue()) ;

map.put(key, value);

}

return map;

}

/*gitHub结束*/

}创建跨域配置类 以防万一出现跨域问题代码语言:java复制import lombok.extern.slf4j.Slf4j;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.cors.CorsConfiguration;

import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import org.springframework.web.filter.CorsFilter;

/**

* ClassName: CorsAutoConfig

*

* @author yangshuai

* @Date: 2021-04-13 14:54

* @Description: $

**/

@Configuration

public class CorsAutoConfig {

@Bean

public CorsFilter corsFilter() {

UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();

CorsConfiguration corsConfiguration = new CorsConfiguration();

corsConfiguration.addAllowedHeader("*");

corsConfiguration.addAllowedMethod("*");

// 表示什么域名跨域 *表示全部都跨域

corsConfiguration.addAllowedOrigin("*");

// 注入进去

urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);

return corsFilter;

}

}创建Logincontroller代码语言:java复制import com.google.gson.Gson;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import top.yangbuyi.QQ.OAuthProperties;

import top.yangbuyi.QQ.vo.QQDTO;

import top.yangbuyi.QQ.vo.QQOpenidDTO;

import top.yangbuyi.common.HttpsUtils;

import javax.management.RuntimeErrorException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.websocket.server.PathParam;

import java.io.IOException;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

import java.util.UUID;

/**

* @description: 杨不易网站:www.yangbuyi.top

* @program: qqlogindemo

* @ClassName: loginController

* @create: 2020-08-18 14:41

* @author: yangbuyi

* @since: JDK1.8

* @loginController: 第三方QQ登陆

**/

@Controller

@Slf4j

@RequestMapping("api")

public class loginController {

/**

* 认证参数

*/

@Autowired

private OAuthProperties oauth;

/**

* 调用QQ登陆接口

* 流程: 先调用接口获取code,在根据code获取access_token,在根据token获取对应的用户信息

* @param response

*/

@GetMapping("/login/oauth")

public void loginQQ( HttpServletResponse response) {

// 重定向访问QQ登录服务器

try {

response.sendRedirect(oauth.getQQ().getCode_callback_uri() + //获取code码地址

"?client_id=" + oauth.getQQ().getClient_id() //appid

+"&state=" + UUID.randomUUID() + //这个说是防攻击的,就给个随机uuid吧

"&redirect_uri=" + oauth.getQQ().getRedirect_uri() +//这个很重要,这个是回调地址,即就收腾讯返回的code码

"&response_type=code");

} catch (IOException e) {

e.printStackTrace();

}

}

/**

* 在qq平台设置的回调地址

*

* 接收回调地址带过来的code码

*

* @param code

* @param request

* @return

*/

@GetMapping("/oauth2")

public String authorizeQQ(String code, HttpServletRequest request) {

HashMap params = new HashMap<>();

params.put("code", code);

params.put("grant_type", "authorization_code");

params.put("redirect_uri", oauth.getQQ().getRedirect_uri());

params.put("client_id", oauth.getQQ().getClient_id());

params.put("client_secret", oauth.getQQ().getClient_secret());

// 获取腾讯access token

Map reulsts = getAccess_token(params);

System.out.println("遍历拿到的数据:");

for (Map.Entry entry : reulsts.entrySet()) {

System.out.println(entry.getKey() + "=" + entry.getValue());

}

System.out.println("遍历完毕");

//到这里access_token已经处理好了

//下一步获取openid,只有拿到openid才能拿到用户信息

String openidContent = HttpsUtils.doGet(oauth.getQQ().getOpenid_callback_uri() + "?access_token=" + reulsts.get("access_token"));

// callback( {"client_id":"101887062","openid":"74DD1353321FD56375F34422D833848D"} );

System.out.println("openidContent: " + openidContent);

//接下来对openid进行处理

//截取需要的那部分json字符串

String openid = openidContent.substring(openidContent.indexOf("{"), openidContent.indexOf("}") + 1);

// json 转 对象

Gson gson = new Gson();

//将返回的openid转换成DTO

QQOpenidDTO qqOpenidDTO = gson.fromJson(openid, QQOpenidDTO.class);

// 封装参数 请求用户信息数据

params.clear();

//设置access_token

params.put("access_token", reulsts.get("access_token"));

//设置openid

params.put("openid", qqOpenidDTO.getOpenid());

//设置appid

params.put("oauth_consumer_key", qqOpenidDTO.getClient_id());

//获取用户信息

String userInfo = HttpsUtils.doGet(oauth.getQQ().getUser_info_callback_uri(), params);

QQDTO qqDTO = gson.fromJson(userInfo, QQDTO.class);

// (正常情况下,在开发时候用openid作为用户名,再自己定义个密码就可以了)

try {

/* 组装数据 */

HashMap map = new HashMap<>();

map.put("user", qqDTO);

map.put("qqOpenidDTO", qqOpenidDTO);

request.setAttribute("map", map);

log.info("user数据:{}" + qqDTO);

log.info("qqOpenidDTO数据:{}" + qqOpenidDTO);

return "home";

} catch (Exception e) {

e.printStackTrace();

return "login";

}

}

/**

* 获取腾讯 access_token

*

* @return

*/

public Map getAccess_token(HashMap params) {

// 认证地址

//获取access_token如:access_token=9724892714FDF1E3ED5A4C6D074AF9CB&expires_in=7776000&refresh_token=9E0DE422742ACCAB629A54B3BFEC61FF

String result = HttpsUtils.doGet(oauth.getQQ().getAccess_token_callback_uri(), params);

//对拿到的数据进行切割字符串

String[] strings = result.split("&");

//切割好后放进map

Map reulsts = new HashMap<>();

for (String str : strings) {

String[] split = str.split("=");

if (split.length > 1) {

reulsts.put(split[0], split[1]);

}

}

return reulsts;

}

}创建QQ参数实体类创建 OAuthProperties 用于配合yml配置文件动态获取参数代码语言:java复制import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

/**

* description: 杨不易网站 :www.yangbuyi.top

* ClassName: OAuthProperties

* create: 2020-06-24 17:06

*

* @author: yangbuyi

* @since: JDK1.8

*

* 获取Code码

**/

@Component

//对应application.yml中,oauth下参数

@ConfigurationProperties(prefix = "oauth")

public class OAuthProperties {

//获取applicaiton.yml下qq下所有的参数

private QQProperties qq = new QQProperties();

public QQProperties getQQ() {

return qq;

}

public void setQQ(QQProperties qq) {

this.qq = qq;

}

}创建 QQProperties 用于请求qq的参数代码语言:java复制import lombok.Data;

import org.springframework.stereotype.Component;

/**

* description: 杨不易网站 :www.yangbuyi.top

* ClassName: QQProperties

* create: 2020-06-24 17:04

*

* @author: yangbuyi

* @since: JDK1.8

*

* 集成第三方登陆 QQ 参数

**/

@Data

@Component

public class QQProperties {

/**

* 你的appid

*/

private String client_id;

/**

* #你的appkey

*/

private String client_secret;

/**

* 你接收响应code码地址

*/

private String redirect_uri;

/**

* 腾讯获取code码地址

*/

private String code_callback_uri;

/**

* 腾讯获取access_token地址

*/

private String access_token_callback_uri;

/**

* 腾讯获取openid地址

*/

private String openid_callback_uri;

/**

* 腾讯获取用户信息地址

*/

private String user_info_callback_uri;

/**

* 要回调到哪个网站

*/

private String redirect_url_index_yby;

private String redirect_url_login_yby;

}创建 QQOpenidDTO 用于获取 access_token、openid代码语言:java复制import lombok.Data;

/**

* description: 杨不易网站 :www.yangbuyi.top

* ClassName: QQOpenidDTO

* create: 2020-06-24 17:19

*

* @author: yangbuyi

* @since: JDK1.8

*

* 用来获取 access_token、openid

**/

@Data

public class QQOpenidDTO {

private String openid;

private String client_id;

}创建QQDTO 接收QQ返回来的json参数代码语言:java复制import lombok.Data;

/**

* description: 杨不易网站 :www.yangbuyi.top

* program: yangbuyi-erp-2020

* ClassName: QQDTO

* create: 2020-06-24 17:20

*

* @author: yangbuyi

* @since: JDK1.8

* @QQDTO: 用于存储QQ服务器返回来的参数

**/

@Data

public class QQDTO {

private String ret; //返回码

private String msg; //如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。

private String nickname; //用户在QQ空间的昵称。

private String figureurl; //大小为30×30像素的QQ空间头像URL。

private String figureurl_1; //大小为50×50像素的QQ空间头像URL。

private String figureurl_2; //大小为100×100像素的QQ空间头像URL。

private String figureurl_qq_1; //大小为40×40像素的QQ头像URL。

private String figureurl_qq_2; //大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100x100的头像,但40x40像素则是一定会有。

private String gender; //性别。 如果获取不到则默认返回"男"

private Integer gendertype; // 性别 数字

private String is_yellow_vip; //标识用户是否为黄钻用户(0:不是;1:是)。

private String vip; //标识用户是否为黄钻用户(0:不是;1:是)

private String yellow_vip_level; //黄钻等级

private String level; //黄钻等级

private String is_yellow_year_vip; //标识是否为年费黄钻用户(0:不是; 1:是)

private String province; // 省

private String city; // 市

}示例

image创建前端请求跳转 controller代码语言:java复制@Controller

@Slf4j

public class RequestController {

@RequestMapping("login")

public String login() {

System.out.println("登陆进来啦");

return "login";

}

@RequestMapping("home")

public String home() {

return "home";

}

}创建前端页面imagelogin.html代码语言:html复制

Title

home.html代码语言:html复制

Title

用户头像:


性别:

启动注意事项必须要打包到服务器启动QQ才能回调

项目部署方案一:

点击package 打包

image复制 项目 和 application.yml 上传到linux服务器

imageimage修改application.yml 中的端口为 80

image运行 Java程序

java -jar qqlogindemo-0.0.1-SNAPSHOT.jar

启动成功

image访问 login 页面

image点击登录 》 QQ扫码或者密码登录 》 登录成功 跳转到 home

imageimageimage到此 从零玩转 第三方登录之QQ登录 就结束了哦。GITEE:https://gitee.com/yangbuyiGITHUB: https://github.com/yangbuyiya个人博客网站: https://www.yangbuyi.top/