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

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

前言

在真正开始对接之前,我们先来聊一聊后台的方案设计。既然是对接第三方登录,那就免不了如何将用户信息保存。首先需要明确一点的是,用户在第三方登录成功之后,
我们能拿到的仅仅是一个代表用户唯一身份的ID(微博是真实uid,QQ是加密的openID)以及用来识别身份的accessToken,当然还有昵称、头像、性别等有限资料,
对接第三方登录的关键就是如何确定用户是合法登录,如果确定这次登录的和上次登录的是同一个人并且不是假冒的。其实这个并不用我们特别操心,就以微博登录为例,
用户登录成功之后会回调一个code给我们,然后我们再拿code去微博那换取 accessToken ,如果这个code是用户乱填的,那这一关肯定过不了,所以,前面的担心有点多余,哈哈。

1. 认识Oauth2.0

现在很多网站都要不管是为了引流也好,为了用户方便也好一般都有第三方账号登陆的需求,今天以QQ登陆为例,来实现一个最简单的第三方登陆。
目前主流的第三方登录都是依赖的Oauth2.0实现的,最常见的就是在各种中小型网站或者App中的QQ登录,微信登录等等。所以我建议想要学习和实现第三方登录同学去了解下这个协议。

必须要域名并且进行备案

比如我的域名: https://yangbuyi.top/
因为腾讯有一个域名认证机制啥的。。。。。。

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

2.1、进行申请开发者身份

image.png

2.2 创建应用

进入应用管理页面创建应用,根据实际需要是创建网站应用还是移动应用,我这里是网站应用:

image.png

image.png

提交成功完步后等待客服审核即可

rBEACGB1T3WAbRYeAABbgxej_d0886.png

这是我网站的基本接口信息

rBEACGB1TzCAWQHoAACuDtnB0M224.png

QQ登陆流程

rBEACGB1T8KAJaADAAHRIhP1RQ892.png

请求参数

rBEACGB1YxyAM74iAAHtcyFsA9Q912.png

创建springboot工程

rBEACGB1UK2AZG4VAABDSvfHG4172.png

依赖

<!-- qq登陆集成 开始 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.11</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.8</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
<!--json转换工具-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!--QQSDK-->
<dependency>
<groupId>net.gplatform</groupId>
<artifactId>Sdk4J</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- qq登陆集成 结束 -->
<!-- 模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 其它配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

创建http请求工具

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<String, Object>());
}
/**
* 发送 GET 请求(HTTP),K-V形式
*
* @param url
* @param params
* @return
*/
public static String doGet(String url, Map<String, Object> 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<String, Object>());
}
/**
* 发送 POST 请求,K-V形式
*
* @param apiUrl API接口URL
* @param params 参数map
* @return
*/
public static String doPost(String apiUrl, Map<String, Object> 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<NameValuePair> pairList = new ArrayList<>(params.size());
for (Map.Entry<String, Object> 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<String,String> getMap(String responseEntity) {
Map<String, String> 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<String,String> getMapByJson(String responseEntity) {
Map<String, String> map = new HashMap<>();
// 阿里巴巴fastjson 将json转换成map
JSONObject jsonObject = JSONObject.parseObject(responseEntity);
for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
String key = entry.getKey();
// 将obj转换成string
String value = String.valueOf(entry.getValue()) ;
map.put(key, value);
}
return map;
}
/*gitHub结束*/
}

创建跨域配置类 以防万一出现跨域问题

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

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<String, Object> 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<String, String> reulsts = getAccess_token(params);
System.out.println("遍历拿到的数据:");
for (Map.Entry<String, String> 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<String, Object> 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<String, String> getAccess_token(HashMap<String, Object> 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<String, String> 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配置文件动态获取参数

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
* <p>
* 获取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的参数

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

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参数

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; // 市
}

示例

rBEACGB1ViqAaRY9AAAP5cTGO3M253.png

创建前端请求跳转 controller

@Controller
@Slf4j
public class RequestController {
@RequestMapping("login")
public String login() {
System.out.println("登陆进来啦");
return "login";
}
@RequestMapping("home")
public String home() {
return "home";
}
}

创建前端页面

rBEACGB1VAmAWUEqAAAZhCVy8Ec239.png

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
登录地址 action="/api/login/oauth"
-->
<form action="/api/login/oauth">
<input type="submit" style="background: red;size: 25px" value="登陆">
</form>
</body>
</html>

home.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="">
<label class="">登陆成功</label>
<div class="">
<p th:text="'openID :' + ${map.qqOpenidDTO.openid}"></p>
<p th:text="'用户名称 :' + ${map.user.nickname}"></p>
用户头像:
<img th:src="${map.user.figureurl_qq_1}" alt="">
<br>
<img th:src="${map.user.figureurl_qq_1}" alt="">
<img th:src="${map.user.figureurl_qq_2}" alt="">
性别:
<p th:text="${map.user.gender}"></p>
<p th:text="${map.user.vip}"></p>
<p th:text="${map.user.yellow_vip_level}"></p>
<p th:text="${map.user.is_yellow_year_vip}"></p>
<p th:text="${map.user.province}"></p>
<p th:text="${map.user.city}"></p>
</div>
</div>
<!--参数列表:-->
<!--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; // 市-->
</body>
</html>

启动注意事项

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

项目部署

方案一:

点击package 打包

rBEACGB1WGKAGsziAAA6rFmT84991.png

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

rBEACGB1WJ2ABZYZAAApYyKRlMc707.png

修改application.yml 中的端口为 80
rBEACGB1WiiAJ570AADOVFxqV48964.png

运行 Java程序

java -jar qqlogindemo-0.0.1-SNAPSHOT.jar

启动成功

rBEACGB1WkAO6uYAAHHR6o4Y0A001.png

访问 login 页面
rBEACGB1WlKANkagAABReRxnILE224.png

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

rBEACGB1WneAMwnQAAEhAT0ylYw886.png

rBEACGB1WxSADXusAAFn4EPmnA908.png

rBEACGB1WuWAeceAAEKl7Xrtx8174.png

到此 从零玩转 第三方登录之QQ登录 就结束了哦。

GITEE:https://gitee.com/yangbuyi

GITHUB: https://github.com/yangbuyiya

个人博客网站: https://www.yangbuyi.top/

春天交流群 :598347590

image.png

posted @   杨不易呀  阅读(737)  评论(2编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示