从零玩转QQ登录-clwzqqdlu
从零玩转第三方登录之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、进行申请开发者身份
2.2 创建应用
进入应用管理页面创建应用,根据实际需要是创建网站应用还是移动应用,我这里是网站应用:
提交成功完步后等待客服审核即可
这是我网站的基本接口信息
QQ登陆流程
请求参数
创建springboot工程
依赖
<!-- 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; // 市 }
示例
创建前端请求跳转 controller
@Controller @Slf4j public class RequestController { @RequestMapping("login") public String login() { System.out.println("登陆进来啦"); return "login"; } @RequestMapping("home") public String home() { return "home"; } }
创建前端页面
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 打包
复制 项目 和 application.yml 上传到linux服务器
修改application.yml 中的端口为 80
运行 Java程序
java -jar qqlogindemo-0.0.1-SNAPSHOT.jar
启动成功
访问 login 页面
点击登录 》 QQ扫码或者密码登录 》 登录成功 跳转到 home
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南