背景
最近第一次使用QQ登录功能,期间遇到这种问题,在网上找了很多资料,大多都是官方的搬运,并没有真正的干料,可能是个人能力问题,遇到了各种麻烦,折腾了几天,最终弄好,在这里记录一下和大家分享,希望对大家有所帮助。
由于官方文档已经对如何使用API接口做出了很明确的说明,所以这里我只是记录我在开发过程中一些步骤中遇到的问题和注意的事项,详细步骤请参考官方文档:http://wiki.connect.qq.com/
开发环境:SpringMVC、QQ登录Java SDK版、Maven、IDEA
应用部署和常见问题解决
访问QQ互联的地址如下:http://connect.qq.com/,这里是需要开发者进行注册登录验证的网址
一、账户登录
注意:登录的账户尽量用公司专用的,以后用于测试的时候,只能这个账户进行测试,这样的话,可以减少不必要的麻烦或者泄露自己的QQ信息,另外在本地是可以进行测试的,网上流传的本地不可测试的说法是不正确的,官方Demo中已经明确给出了具体的本地测试方法:(本地测试未进行验证,不做研究)
如果在本机tomcat或其他服务器下部署请配置本地host文件:127.0.0.1 您的回调域名直接部署运行,将sdk4j_demo目录中的web目录直接放在tomcat服务器的webapp目录即可,配置conf/server.xml文件中的host的context<Context docBase="web" path="/" privileged="true" antiResourceLocking="false"></Context>
并将webapp目录下的Root 文件夹暂时移除。
其他服务器请参照服务器自身部署方法。**请将服务器的端口号配置至80端口**。
配置host:127.0.0.1 您的回调域名 访问首页 您的回调域名/index.jsp
网站首页 index.jsp 引导用户到 IndexServlet
IndexServlet 用到了 SDK中的 com.qq.connect.oauth.Oauth.getAuthorizeURL(..) 方法来获取应该引导用户跳转的地址
。。。(详见Demo中的ReadMe.txt文件)
二、注册开发者账户
如果已经注册,则不会有该步骤,没有的话根据相应的需求进行注册
三、创建应用
首先可以根据官方案例进行编写自己的项目,http://qzonestyle.gtimg.cn/qzone/vas/opensns/res/doc/qqConnect_Server_SDK_java_v2.0.zip
(或者可访问:http://wiki.connect.qq.com/sdk%E4%B8%8B%E8%BD%BD)
项目代码完成之后,可以继续下边的内容:
在这一块我相信是很多人遇到的问题,第一个是网站地址该如何填写,另一个是回调地址的填写,尽管填写完成还会有相应的报错,下边做一下我的分析:
1、网站地址的选取:首先是官网的首页或登陆界面,在域名解析的时候必须为80端口(我尝试过其他端口是不可以的)
上图是一个域名的解析,例如:www.abc.com是我的域名,111.23.23.244是我的主机,图中有三条记录
第一条是将域名和主机IP地址进行绑定;
第二条是将二级域名dubboadmin进行解析,即www.dubboadmin.abc.com;
第三条是将二级域名security进行解析,即www.security.abc.com;;
此时可以看出,dubboadmin和security都是二级域名,一个是通过A记录一个是通过隐形URL的类型,这是因为,在默认的域名解析中,如果通过A记录来绑定域名的话,默认的是绑定到IP的80端口,即111.23.23.244:80,但是一个IP绑定主机上的端口还有很多,一个IP主机机器上可以运行多个项目,这样的话我们就通过隐形URL的方式来将不同的二级域名绑定到不同的端口上,即是将dubboadmin绑定到http://www.abc.com:8012端口上.
我在做QQ登录的时候,亲测过使用其他端口的不可行,请看下图:
这个是我使用的该域名下绑定的8011端口,打开网页时的界面,这里边明显的是一个framenset将doucument中的内容进行包裹,在最外层的html下的head中并没有需要添加的meta信息,而真正的我们的网页信息,是document包裹内的东西,游览前只是根据非80端口的服务器网页内容做了一次封装显示在网页中,而qq在验证网站的时候,找的是该网页head中的meta信息,因此会验证失败。这一点也在官方Demo的ReadMe.txt文件中明确指出了使用80端口。
所以,如果你在开发的过程中也是出现使用IP非80端口绑定的域名遇到了验证失败这个问题,请使用80端口进行测试。
最后,根据80端口,我绑定的域名是www.security.abc.com,但是还要注意的是,对于任何一个用户在访问www.security.abc.com地址的时候,有可能会让用户登录,那么会跳转到一个登录界面,例如我的就是当访问www.security.abc.com的时候,他会自动跳到security.abc.com/login.do让用户登录,那我我最终的网站地址就是:http://security.abc.com/login.do,因为,这个网址也是QQ在验证网站的时候进行访问的链接(可以打log进行查看),因此使用http://security.abc.com/ 是不可以的。(虽然,我在服务器处理的是,访问www.security.abc.com如果用户没有登录会自动跳转到http://security.abc.com/login.do,但是直接写http://security.abc.com/验证是失败的,具体不清楚是不是qq在验证的时候直接找http://security.abc.com/下的界面,这个还需后边的测试去验证)
同样的在这个login界面,也就是http://security.abc.com/login.do所指向的界面中添加QQ登录按钮,和网站验证时需要填写在head标签中的meta信息,例如:
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta property="qc:admins" content="1133201050a16asdfhjsdf7sd5f6567647"/><title>QQ登录测试</title>
</head>
<div class="form-group input-group" style="margin-top: 45px;"><a href="/qqlogin.do">请使用你的QQ账号登陆</a>
</div>
这里的/qqlogin.do是进行登录处理的controller地址,在后边会说
2、回调地址的选择
回调地址的选择是在用户通过QQ授权之后,我们需要对用户信息进行保存的地址,由于使用的是SpringMVC,所以后缀的时候有个do,例如我的回调操作如下:(代码为官方Demo案例,可以直接使用)
@Controller
@RequestMapping
public class LoginController {@Injectprivate Oauth qqOauth;/*** 进行QQ登录,这里就是前台界面中访问的那个超链接的处理Controller*/@RequestMapping(value = "/qqlogin")public void QQLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {response.setContentType("text/html;charset=utf-8");try {response.sendRedirect(qqOauth.getAuthorizeURL(request));} catch (QQConnectException e) {e.printStackTrace();}}/*** 用于QQ登录的回调,进行用户信息的保存操作等*/@RequestMapping(value = "/afterQQLogin")protected void afterQQLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {response.setContentType("text/html; charset=utf-8");PrintWriter out = response.getWriter();try {AccessToken accessTokenObj = (new Oauth()).getAccessTokenByRequest(request);String accessToken = null, openID = null;long tokenExpireIn = 0L;//这还有很多代码,是在上边的那个官方案例中给出的,或者在最下边的代码中,请参考使用} catch (QQConnectException e) {}}
}
因此我的回调地址是:http://security.abc.com/afterQQLogin.do
如果你的项目代码中完成了以上部分的话,可以将项目部署到真实的域名下,然后进行验证。
3、项目的配置文件
如果上述过程OK,则在官方demo中还有一个名为:qqconnectconfig.properties的配置文件,这个是需要配置的:
app_ID = 34234123 #自己申请的id
app_KEY = ecfd12341234qewrqwer69db492e1ca #自己申请的key
redirect_URI = http://security.abc.com/afterQQLogin.do #回调地址
scope=get_user_info,add_topic,add_one_blog,add_album,upload_pic,list_album,add_share,check_page_fans,add_t,add_pic_t,del_t,get_repost_list,get_info,get_other_info,get_fanslist,get_idollist,add_idol,del_ido,get_tenpay_addr
#下边还有很多,因为是不需要修改的,所以不具体列出
这里提示:别忘了引用qq登录的jar包
官方Demo中介绍的是该文件要放在src目录下,这是在传统的通过MyEclipse中创建的静态Web项目的做法,如果使用的是Spring框架的话,把该文件放在src目录下的话会出现bean加载失败的问题,则需要将该文件放在resource目录下,这样的话,在进行项目启动的时候就可以加载到。
另外,如果将该文件放在了正确的位置,还是出现bean未加载的问题的话,如果出现下边的错误:
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.qq.connect.oauth.Oauth org.albert.security.controller.LoginController.qqOauth; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.qq.connect.oauth.Oauth] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@javax.inject.Inject()}
我的解决方法是手动配置该Oauth,在spring配置文件的相应位置放入
<bean id="qqOauth" class="com.qq.connect.oauth.Oauth"/>
在使用的位置添加:
@Injectprivate Oauth qqOauth;
并将案例中response.sendRedirect(new Oauth().getAuthorizeURL(request));的代码对应的改为response.sendRedirect(qqOauth.getAuthorizeURL(request));(可以将Demo中的代码和最下边我提供的代码进行比较,可以发现修改的地方,就是在Oauth类实例化的时候进行手动的装配bean)
这样的话,在我的项目中可以跑起来,QQ登录的功能也可以使用。
其他问题
1、官方对应错误返回码文档说明如下:http://wiki.connect.qq.com/公共返回码说明
2、如果使用IDEA+maven项目的开发方式的话,那么怎么引用qq的依赖jar哪?
过程如下:在WEB-INF 目录下创建一个lib目录,将该jar放进去,然后再对应的POM文件中加入:
<dependency><groupId>javabuilder</groupId><artifactId>javabuilder</artifactId><version>1.0</version><scope>system</scope><systemPath>${project.basedir}/src/main/webapp/WEB-INF/lib/Sdk4J.jar</systemPath>
</dependency>
systemPath为自己对应的位置,即可完成对jar的依赖,另外别忘了将在Sdk4J.jar上右键将改jar加入到classpath中。
3、javax.net.ssl.SSLKeyException
在使用QQ官方的api方法时,如果在QQ api代码中报出一下异常
javax.net.ssl.SSLKeyException: [Security:090477]Certificate chain received from ebanktest.95559.com.cn - 124.74.249.16 was not trusted causing SSL handshake failure
请将jdk的版本设置为sun jdk而不是openjdk
[root@VM_92_170_centos home]# java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
[root@VM_92_170_centos home]#
—————————————————分割线——————————————————-
另附我在项目中是使用的Controller:
@Controller
@RequestMapping
public class LoginController {private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);@Injectprivate Oauth qqOauth;/*** 进行QQ登录* @param request* @param response* @throws IOException*/@RequestMapping(value = "/qqlogin")public void QQLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {response.setContentType("text/html;charset=utf-8");try {response.sendRedirect(qqOauth.getAuthorizeURL(request));} catch (QQConnectException e) {e.printStackTrace();}}/*** 用于QQ登录的回调*/@RequestMapping(value = "/afterQQLogin")protected void afterQQLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {response.setContentType("text/html; charset=utf-8");PrintWriter out = response.getWriter();try {AccessToken accessTokenObj = (new Oauth()).getAccessTokenByRequest(request);String accessToken = null, openID = null;long tokenExpireIn = 0L;if (accessTokenObj.getAccessToken().equals("")) {
// 我们的网站被CSRF攻击了或者用户取消了授权
// 做一些数据统计工作System.out.print("没有获取到响应参数");} else {accessToken = accessTokenObj.getAccessToken();tokenExpireIn = accessTokenObj.getExpireIn();request.getSession().setAttribute("demo_access_token", accessToken);request.getSession().setAttribute("demo_token_expirein", String.valueOf(tokenExpireIn));// 利用获取到的accessToken 去获取当前用的openid -------- startOpenID openIDObj = new OpenID(accessToken);openID = openIDObj.getUserOpenID();out.println("欢迎你,代号为 " + openID + " 的用户!");request.getSession().setAttribute("demo_openid", openID);
// out.println("<a href=" + "/shuoshuoDemo.html" + " target=\"_blank\">去看看发表说说的demo吧</a>");// 利用获取到的accessToken 去获取当前用户的openid --------- endout.println("<p> start -----------------------------------利用获取到的accessToken,openid 去获取用户在Qzone的昵称等信息 ---------------------------- start </p>");UserInfo qzoneUserInfo = new UserInfo(accessToken, openID);UserInfoBean userInfoBean = qzoneUserInfo.getUserInfo();out.println("<br/>");if (userInfoBean.getRet() == 0) {out.println(userInfoBean.getNickname() + "<br/>");out.println(userInfoBean.getGender() + "<br/>");out.println("黄钻等级: " + userInfoBean.getLevel() + "<br/>");out.println("会员 : " + userInfoBean.isVip() + "<br/>");out.println("黄钻会员: " + userInfoBean.isYellowYearVip() + "<br/>");out.println("<image src=" + userInfoBean.getAvatar().getAvatarURL30() + "/><br/>");out.println("<image src=" + userInfoBean.getAvatar().getAvatarURL50() + "/><br/>");out.println("<image src=" + userInfoBean.getAvatar().getAvatarURL100() + "/><br/>");} else {out.println("很抱歉,我们没能正确获取到您的信息,原因是: " + userInfoBean.getMsg());}out.println("<p> end -----------------------------------利用获取到的accessToken,openid 去获取用户在Qzone的昵称等信息 ---------------------------- end </p>");out.println("<p> start ----------------------------------- 验证当前用户是否为认证空间的粉丝------------------------------------------------ start <p>");PageFans pageFansObj = new PageFans(accessToken, openID);PageFansBean pageFansBean = pageFansObj.checkPageFans("97700000");if (pageFansBean.getRet() == 0) {out.println("<p>验证您" + (pageFansBean.isFans() ? "是" : "不是") + "QQ空间97700000官方认证空间的粉丝</p>");} else {out.println("很抱歉,我们没能正确获取到您的信息,原因是: " + pageFansBean.getMsg());}out.println("<p> end ----------------------------------- 验证当前用户是否为认证空间的粉丝------------------------------------------------ end <p>");out.println("<p> start -----------------------------------利用获取到的accessToken,openid 去获取用户在微博的昵称等信息 ---------------------------- start </p>");com.qq.connect.api.weibo.UserInfo weiboUserInfo = new com.qq.connect.api.weibo.UserInfo(accessToken, openID);com.qq.connect.javabeans.weibo.UserInfoBean weiboUserInfoBean = weiboUserInfo.getUserInfo();if (weiboUserInfoBean.getRet() == 0) {//获取用户的微博头像----------------------startout.println("<image src=" + weiboUserInfoBean.getAvatar().getAvatarURL30() + "/><br/>");out.println("<image src=" + weiboUserInfoBean.getAvatar().getAvatarURL50() + "/><br/>");out.println("<image src=" + weiboUserInfoBean.getAvatar().getAvatarURL100() + "/><br/>");//获取用户的微博头像 ---------------------end//获取用户的生日信息 --------------------startout.println("<p>尊敬的用户,你的生日是: " + weiboUserInfoBean.getBirthday().getYear()+ "年" + weiboUserInfoBean.getBirthday().getMonth() + "月" +weiboUserInfoBean.getBirthday().getDay() + "日");//获取用户的生日信息 --------------------endStringBuffer sb = new StringBuffer();sb.append("<p>所在地:" + weiboUserInfoBean.getCountryCode() + "-" + weiboUserInfoBean.getProvinceCode() + "-" + weiboUserInfoBean.getCityCode()+ weiboUserInfoBean.getLocation());//获取用户的公司信息---------------------------startArrayList<Company> companies = weiboUserInfoBean.getCompanies();if (companies.size() > 0) {//有公司信息for (int i = 0, j = companies.size(); i < j; i++) {sb.append("<p>曾服役过的公司:公司ID-" + companies.get(i).getID() + " 名称-" +companies.get(i).getCompanyName() + " 部门名称-" + companies.get(i).getDepartmentName() + " 开始工作年-" +companies.get(i).getBeginYear() + " 结束工作年-" + companies.get(i).getEndYear());}} else {//没有公司信息}//获取用户的公司信息---------------------------endout.println(sb.toString());} else {out.println("很抱歉,我们没能正确获取到您的信息,原因是: " + weiboUserInfoBean.getMsg());}out.println("<p> end -----------------------------------利用获取到的accessToken,openid 去获取用户在微博的昵称等信息 ---------------------------- end </p>");}} catch (QQConnectException e) {}}
}
登录连接:
<div class="form-group input-group" style="margin-top: 45px;"><a href="/qqlogin.do">请使用你的QQ账号登陆</a>
</div>