前言

个人网站最近增加了评论功能,为了方便用户不用注册就可以评论,对接了QQ和微博这2大常用软件的一键登录,总的来说其实都挺简单的,可能会有一点小坑,但不算多,完整记录下来方便后来人快速对接。

后台设计

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

另外一个问题就是如何和现有用户系统打通,有的网站在用户已经登录成功之后还要用户输入手机号和验证码,或者要用户重新注册账号和密码来绑定第三方账户,感觉这种实现用户体验非常差,碰到这种网站我一般都是直接关掉,都已经登录了还让用户注册,什么鬼!由于我做的是评论功能,我并不希望评论用户和现有用户表打通,所以就不存在这件事了,如果想打通的话,我觉得无非就是登录成功之后默认往老用户表插入一条数据,然后和OpenUser表关联起来,判断用户是否登录时把OpenUser的鉴权也加进去就OK了。

本文的后台以Java为例。

数据库设计

再来说说数据库设计,为了系统的扩展性,我有一个专门的OpenUser表用来存放第三方登录用户,主要字段如下:

字段名 字段含义
id 主键
openType 第三方类型,比如qqweibo
openId 代表用户唯一身份的ID
accessToken 调用接口需要用到的token,比如利用accessToken发表微博等,如果只是对接登录的话,这个其实没啥用
expiredTime 授权过期时间,第三方登录授权都是有过期时间的,比如3个月
nickname 昵称
avatar 头像

这样设计理论上就可以无限扩展了。

鉴权流程

这里我只是说说我的方案,把accessToken写入cookie肯定是不安全的,因为accessToken相当于是第三方网站的临时密码,被别人窃取了就可以随意拿来干坏事了。可以在用户登录成功之后我们自己生成一个token,这样的token即使泄露了顶多就是被人拿来随意评论,损失不大,但是如果accessToken被泄露了,以微博为例,人家可以利用这个accessToken随意发微博、删微博、加关注等等,很危险。当然,如果不想token泄露的话也可以通过绑定IP等方式来限制。

鉴权的话就是首先判断cookie中是否有我们自己的token,然后判断是否合法,合法再判断第三方授权是否已过期等等。

对接QQ登录

实名认证

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

创建应用

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

第一步:

第二步:

提交完之后会自动提交审核,基本上就是审核你的资料和备案的资料是否一致,所有资料必须和备案资料一模一样,否则审核不会通过:

当然,这些资料后面还是可以修改的。申请成功之后你会得到appIdappKey

引导用户登录

这里可以下载一些视觉素材,在页面合适位置放一个QQ登录按钮,点击时引导用户进入授权页面:

代码:

/**
* 封装一个居中打开新窗口的方法
*/
function openWindow(url, width, height)
{
width = width || 600;
height = height || 400;
var left = (window.screen.width - width) / 2;
var top = (window.screen.height - height) / 2;
window.open(url, "_blank", "toolbar=yes, location=yes, directories=no, status=no, menubar=yes, scrollbars=yes, resizable=no, copyhistory=yes, left="+left+", top="+top+", width="+width+", height="+height);
} function qqLogin()
{
var qqAppId = '424323422'; // 上面申请得到的appid
var qqAuthPath = 'http://www.test.com/auth'; // 前面设置的回调地址
var state = 'fjdslfjsdlkfd'; // 防止CSRF攻击的随机参数,必传,登录成功之后会回传,最好后台自己生成然后校验合法性
openWindow(`https://graph.qq.com/oauth2.0/authorize?response_type=token&client_id=${qqAppId}&redirect_uri=${encodeURIComponent(qqAuthPath)}&state=${state}`);
}

然后会打开一个授权页面,这个页面大家应该都熟悉:

然后到了这里我就碰到一个问题了,官方文档写的是登录成功之后首先会回传一个code,然后再拿code调接口换取accessToken,然后我试了很多次也换过2个账号发现每次都是直接返回了accessToken,帮我省了一步了,不知道是什么情况,郁闷。

拿到accessToken

现在假设我们都是直接拿到accessToken(因为我暂时还没搞明白QQ为啥会直接返回,跟文档说的不一样),但是授权回调时accessToken会被放在#后面,URL地址中的hash值好像不会被传到后台(貌似是这样,如有不正确欢迎评论指正),所以只能写一个下面这样的临时页面:

/**
* QQ鉴权
* @param request
* @param response
* @throws Exception
*/
@RequestMapping("/authqq")
public void authQQ(HttpServletRequest request, HttpServletResponse response) throws Exception
{
// QQ登录有点特殊,参数放在#后面,后台无法获取#后面的参数,只能用JS做中间转换
String html = "<!DOCTYPE html>" +
"<html lang=\"zh-cn\">" +
"<head>" +
" <title>QQ登录重定向页</title>" +
" <meta charset=\"utf-8\"/>" +
"</head>" +
"<body>" +
" <script type=\"text/javascript\">" +
" location.href = location.href.replace('#', '&').replace('auth_qq', 'auth_qq_redirect');" +
" </script>" +
"</body>" +
"</html>";
response.getWriter().print(html);
}

获取openId

根据accessToken调接口获取用户的openId,特别注意这个openId是相对于QQ号+appId唯一的,换句话说同一个QQ号登录2个不同appId时获取到的openId是不同的。顺便说一句,QQ登录的相关接口做的还真够“随便”的,全部都是最简单的get请求,所以对接起来非常顺利。

直接看代码:

// 根据accessToken换取openId
// 错误示例:callback( {"error":100016,"error_description":"access token check failed"} );
// 正确示例:callback( {"client_id":"10XXXXX49","openid":"CF2XXXXXXXX9F4C"} );
String result = HttpsUtil.get("https://graph.qq.com/oauth2.0/me?access_token=" + accessToken);
Map<String, Object> resp = parseQQAuthResponse(result); // 这个方法就是把结果转Map Integer errorCode = (Integer)resp.get("error");
String errorMsg = (String)resp.get("error_description");
String openId = (String)resp.get("openid");
if(errorCode != null) return new ErrorResult(errorCode, "获取QQ用户openId失败:"+errorMsg);

获取用户头像昵称等信息

// 获取用户昵称、头像等信息,{ret: 0, msg: '', nickname: '', ...} ret不为0表示失败
result = HttpsUtil.get("https://graph.qq.com/user/get_user_info?access_token="+accessToken+"&oauth_consumer_key="+appId+"&openid="+openId);
resp = JsonUtil.parseJsonToMap(result); Integer ret = (Integer)resp.get("ret");
String msg = (String)resp.get("msg");
if(ret != 0) return new ErrorResult("获取用户QQ信息失败:"+msg); // 用户昵称可能存在4个字节的utf-8字符,MySQL默认不支持,直接插入会报错,所以过滤掉
String nickname = StringUtil.filterUtf8Mb4((String)resp.get("nickname")).trim(); // 这个方法可以自行百度
// figureurl_qq_2=QQ的100*100头像,figureurl_2=QQ 100&100空间头像,QQ头像不一定有,空间头像一定有
String avatar = (String)resp.get("figureurl_qq_2");
if(StringUtil.isBlank(avatar)) avatar = (String)resp.get("figureurl_2");
String gender = (String)resp.get("gender");

注意事项

到了这一步基本上涉及第三方的就结束了,是不是很简单?后面无非就是如何插入数据库、如何保存token、写入session等。

有几点注意事项:

  • 需要注意数据库中是否已经有改用户,没有的添加,有的修改,不要重复添加了;
  • QQ昵称昵称有各种奇奇怪怪的字符,包括emoji,MySQL默认没有开启utf8mb4,直接插入会报错,所以需要过滤掉;
  • 需要做好对各种错误的兼容;
  • 接口会同时返回QQ头像和空间头像,QQ头像不一定有,空间头像一定有;
  • 回调地址必须和申请的域名一致,否则会报错。
  • QQ互联有个特大的bug,有时候显示已登录但是点击授权管理一直报错,此时只需要退出重新登录即可;
  • 授权之后用户可能会在过期之前提前取消授权;

相关文档官网已经写得比较细了,但是比较乱:http://wiki.connect.qq.com/

对接微博登录

实名认证

这个我就不具体讲了,登录 http://open.weibo.com/ 很容易找到相关入口,注册成为开发者,实名认证,一模一样的。

创建应用

点击链接 http://open.weibo.com/apps/new?sort=web 创建web应用:

创建成果后完善相关信息,主要是下面这些:

我就不一一介绍了,都看得懂。

微博登录不需要网站一定要备案,但对网站本身有一定要求,不能弄一个空壳网站让人家去审核,肯定审核不通过的。

有关微博的对接可以参考我好几年前写的一篇文章:【新手入门篇】新浪微博应用开发之Java入门篇

引导用户登录

微博视觉素材下载在这里,页面合适位置放一个登录按钮:

function weiboLogin()
{
let weiboAppId = '432432';
let weiboAuthPath = 'http://www.test.com/authweibo';
openWindow(`https://api.weibo.com/oauth2/authorize?client_id=${weiboAppId}&response_type=code&redirect_uri=${encodeURIComponent(weiboAuthPath)}`);
}

微博登录有一个好处,第一次登录需要授权,后面第二次登录时只会一闪而过自动就登录成功了,都不需要点一下,用户体验非常好,看下图:

获取accessToken

登录成功会返回一个code,根据code换取accessToken:

String params = "client_id=" + appId
+ "&client_secret=" + appSecret
+ "&grant_type=authorization_code"
+ "&redirect_uri=" + URLUtil.encode(authPath)
+ "&code=" + code;
// 用code换取accessToken
String result = HttpsUtil.post("https://api.weibo.com/oauth2/access_token", params);
Map<String, Object> resp = JsonUtil.toObject(result, new TypeReference<Map<String, Object>>(){}); Integer errorCode = (Integer)resp.get("error_code");
String error = (String)resp.get("error");
String errorMsg = (String)resp.get("error_description");
if(errorCode != null && errorCode != 0) return new ErrorResult(errorCode, error + (errorMsg==null?"":errorMsg));
String accessToken = (String)resp.get("access_token");
String uid = (String)resp.get("uid"); // 这个uid就是微博用户的唯一用户ID,可以通过这个id直接访问到用户微博主页
int expires = (Integer)resp.get("expires_in"); // 有效期,单位秒

获取用户头像等信息

// 用uid和accessToken换取用户信息
String result = HttpsUtil.get("https://api.weibo.com/2/users/show.json?access_token="+accessToken+"&uid="+uid);
Map<String, Object> resp = JsonUtil.toObject(result, new TypeReference<Map<String, Object>>(){}); errorCode = (Integer)resp.get("error_code");
error = (String)resp.get("error");
errorMsg = (String)resp.get("error_description");
if(errorCode != null && errorCode != 0) return new ErrorResult(errorCode, error + (errorMsg==null?"":errorMsg)); String nickname = (String)resp.get("screen_name");
// 微博180*180高清头像
String avatar = (String)resp.get("avatar_large");
String gender = (String)resp.get("gender");
gender = "m".equals(gender) ? "男" : ("f".equals(gender) ? "女" : "");

至此涉及第三方的东西都完了,剩下的就是用户自己保存到数据库、写入token、保存session、以及鉴权接口开发了。

注意事项

  • 微博接口都有频率限制,不过一般不会超过;
  • 需做好错误兼容;
  • 微博直接返回的uid,可以根据这个uid直达用户微博主页 https://weibo.com/u/xxxxx ,所以可以把用户头像链接到这里;
  • 其实也有现成的js-sdk,可以根据自己实际需要选择是否使用;
  • 嗯,我再想想还有没有,哦,对了,微博的接口是https,并且是post,需要注意;

相关资源

如何从零开始对接第三方登录(Java版):QQ登录和微博登录的更多相关文章

  1. Java版 QQ空间自动登录无需拷贝cookie一天抓取30WQQ说说数据&流程分析

    QQ空间说说抓取难度比较大,花了一个星期才研究清楚! 代码请移步到GitHub GitHub地址:https://github.com/20100507/Qzone [没有加入多线程,希望你可以参与进 ...

  2. 对接第三方平台JAVA接口问题推送和解决

    前言 本节所讲为实际项目中与第三方对接出现的问题最后还是靠老大解决了问题以此作为备忘录,本篇分为三小节,一小节解析Java加密接口数据,二小节解析XML文件需注意问题,最后一节则是请求Java Soa ...

  3. Asp.Net微信登录-电脑版扫描二维码登录

    像京东,一号店等网站都实现了用微信来登录的功能,就是用手机上的微信扫一扫网站上的二维码,微信上确认后,即可自动用微信的帐号登录网站. 一.创建网站应用 在微信开放平台创建一个网站应用 https:// ...

  4. C#微信登录-电脑版扫描二维码登录

    像京东,一号店等网站都实现了用微信来登录的功能,就是用手机上的微信扫一扫网站上的二维码,微信上确认后,即可自动用微信的帐号登录网站. 一.创建网站应用 在微信开放平台创建一个网站应用 https:// ...

  5. django 注册、登录及第三方接口程序(4):扩展邮箱注册,登录,微博登录

    1.邮箱注册 这里需要扩展User,两种解决办法,1,注册时将email字段内容赋给username,这种瞒天过海型的,另一种就是扩展user,这里介绍django1.5的扩展方法. 1.settin ...

  6. java集成网站微信,微博,qq登录

    微信 WechatConfig.java package com.meeno.chemical.common.sdk.wechat.config; import org.springframework ...

  7. iOS开发UI篇—模仿ipad版QQ空间登录界面

    iOS开发UI篇—模仿ipad版QQ空间登录界面 一.实现和步骤 1.一般ipad项目在命名的时候可以加一个HD,标明为高清版 2.设置项目的文件结构,分为home和login两个部分 3.登陆界面的 ...

  8. java 制作QQ登录界面

    package org.eclipse.wb.swing; import java.awt.BorderLayout;import java.awt.EventQueue; import javax. ...

  9. dedecms织梦第三方登录插件-QQ登录、微博登录、微信登录

    织梦程序集成第三方QQ登录.微博登录.微信登录,获取QQ.微博.微信,并存储至数据库,一键注册为网站会员,不用再次填写绑定信息,方便粘贴用户更强. 织梦第三方登录效果 第三方登录插件特点 1.所有文件 ...

随机推荐

  1. 数据库表反向生成(一) MyBatis-generator与IDEA的集成

    在Dao层,通过数据库表反向生成,可以节省我们很多的精力,把更多的精力投入复杂的业务中. 数据库表反向生成,指的是通过数据库如mysql中的库表schema生成dao层读写表的基础代码,包括model ...

  2. Javascript 面向对象编程—封装

      前  言 Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象.但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类) ...

  3. 通过 PackageManager 获得你想要的 App 信息

    一.前言 开门见山,开篇明义.有些场景下,我们会需要获取一些其它 App 的各项信息,例如:App 名称,包名.Icon 等.这个时候就需要使用到 PackageManager 这个类了. 本篇就 P ...

  4. JavaScript观察者模式

    观察者模式观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得 ...

  5. Java常用类(三)之StringBuffer与StringBuidler

    前言 前面一篇给大家介绍了String类,这个我们经常会用到的一个类,那这一篇给大家分享的是StringBuffer与StringBuidler.等下我也会比较他们三个之间的区别 一.StringBu ...

  6. ubuntu16.04 配置opensips服务器并编译pjsip测试

    一.版本相关 1) ubuntu版本: 16.04 2) opensips版本:2.1.2 3) pjsip版本 :2.5.5 4) ffmpeg版本:3.3.4 5) sdl版本:2.0 6) vs ...

  7. jquery的2.0.3版本源码系列(7):3043行-3183行,deferred延迟对象,对异步的统一管理

    目录 part1 deferred延迟对象 part2  when辅助方法 网盘源代码 链接: https://pan.baidu.com/s/1skAj8Jj 密码: izta part1 defe ...

  8. js excel 列表导出

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  9. iOS之创建一个常驻线程

    // 当创建一个线程,并且希望它一直存在时,但往往我们创建的线程都是执行完成之后也就停止了,不能再次利用,那么如何创建一个线程可以让他可以再次工作呢,这个时候就需要使用到RunLoop了.下面的是我写 ...

  10. Java基础笔记13

    1.集合与对象数组的区别 集合与对象数组共同点:都是存放对象的容器: 区别在于:①集合是没有长度限制的:②集合容器中没有类型的限制. 2.集合(都在Java.util包下) 常用的集合:Collect ...