最新文章
SPRINGBOOT之QQ登录
最近在网站上做了一个QQ登录OAuth2.0接入的功能,下面我就来分享一下如何实现QQ登录。首先在开发之前我们需要为QQ登录做如下准备:1、登录QQ互联https://connect.qq.com/成为开发者(成为开发者需要填写资料,审核时间大概2天吧)。2、有了开发者账号之后就能创建应用,这时会要求你填写网站信息,之后又会进入审核阶段(本人当时几个小时就审核过了)。接下来就是开发阶段了根据QQ互联官方网站可以知道QQ登录开发流程如下:1、获取appid和appkey,appid和appkey在你创建的应用信息中查找。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登录按钮三、接下来就是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分钟。@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 Token(2小时有效期);获取QQ用户的openid;通过openid访问QQ用户信息。1)回调controller@GetMapping("/login/qq/callback")public String qqCallbackUri(String code) { loginService.qqLogin(code); return "/"; //返回地址} 2)回调service,获取用户信息。@Override@Transactionalpublic 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登录就分享完了,感谢大家的支持!
Mar 20, 2019 9:40:40 PM
278