快速接入 GitHub、QQ 第三方登录方式

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 快速接入 GitHub、QQ 第三方登录方式

本文提及第三方登录涉及到 OAuth2.0,关于 OAuth2.0 的理论基础参考阮一峰老师的《理解 OAuth 2.0》,其中关于授权码模式就是本篇文章的重点,如想看这篇理论基础自行百度即可。

本文着重于代码,关于理论不再赘述,关于不同公司的三方登录流程,只要遵循 OAuth2.0 规范,都大同小异。本文介绍 GitHub 和 QQ 两种,因为这两种无需审核,即可食用。历史也发布过 Spring Boot 的其他实战,可以关注微信公众号「Java后端」回复「666」下载技术栈手册。

一、GitHub 登录

1.1 注册应用

进入 Github 的 Setting 页面,点击 Developer settings,如图所示:

image.png

进入后点击 New Oauth App,如图所示:

image.png

在其中填写主页 URL回调 URL,回调 URL 尤为重要,如果不太明白可以先和我一致。

image.png

点击注册后,上方会生成 Client IDClient Secret,这两个后面要用到。

image.png

1.2 HTML 页面

页面十分简单,只有两个跳转链接:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>三方登录</title>
</head>
<body>
    <h1>三方登录Demo</h1>
    <div>
        <a href="/githubLogin">GitHub登录</a>
        <a href="/qqLogin">QQ登录</a>
    </div>
</body>
</html>

1.3 Github 登录方法

在这个方法中,我们需要访问 GitHub 的认证服务器,使用 Get 请求,这里使用重定向来实现。遵循 Oauth 2.0 规范,需要携带以下参数:

  • response_type :对于授权码模式,该值固定为 code
  • client_id :注册应用时的 Client ID
  • state :回调时会原样返回
  • redirect_uri : 回调 URL,注册应用时填写的

这里的 state 参数我要额外说明下,因为该参数会在后面的回调 URL 中被原样携带回来,绝大多数的开发者会忽略该字段,阮一峰老师的文章也没有着重提及这一点。但是忽略该参数是会导致 CSRF攻击的,在回调函数中应当对该字段进行校验!

关于如何校验,我一开始的想法是使用 session 来存储 state 进行校验的,但是我发现使用重定向后 session 不是同一个 session,方案一失败。然后我想通过 ajax 请求,在页面中使用 window.location.href 方法跳转到认证服务器,使用 session 存储,但是很不幸这样也不是同一个 session,方案二失败。最后我的解决办法是使用 redis 缓存,使用 set 存储,回调时判断是否存在。当然你也可以用 HashMap 来存储,这也是一个解决办法。关于 Redis,可以参考:https://jitwxs.cn/e331e26a.html

private static String GITHUB_CLIENT_ID = "0307dc634e4c5523cef2";
private static String GITHUB_CLIENT_SECRET = "707647176eb3bef1d4c2a50fcabf73e0401cc877";
private static String GITHUB_REDIRECT_URL = "http://127.0.0.1:8080/githubCallback";
@RequestMapping("/githubLogin")
public void githubLogin(HttpServletResponse response) throws Exception {
  // Github认证服务器地址
  String url = "https://github.com/login/oauth/authorize";
  // 生成并保存state,忽略该参数有可能导致CSRF攻击
  String state = oauthService.genState();
  // 传递参数response_type、client_id、state、redirect_uri
  String param = "response_type=code&" + "client_id=" + GITHUB_CLIENT_ID + "&state=" + state
          + "&redirect_uri=" + GITHUB_REDIRECT_URL;
  // 1、请求Github认证服务器
  response.sendRedirect(url + "?" + param);
}

1.4 Github 回调方法

在上一步中,浏览器会被跳转到 Github 的授权页,当用户登录并点击确认后,GitHub认证服务器会跳转到我们填写的回调URL中,我们在程序中处理回调。在回调方法中,步骤如下:1. 首先验证 state 与发送时是否一致,如果不一致,可能遭遇了 CSRF 攻击。2. 得到 code,向 GitHub 认证服务器申请令牌(token)  这一步使用模拟的 POST 请求,携带参数包括:

  • grant_type :授权码模式固定为 authorization_code
  • code :上一步中得到的 code
  • redirect_uri :回调URL
  • client_id :注册应用时的Client ID
  • client_secret :注册应用时的Client Secret

3. 得到令牌(access_token)和令牌类型(token_type),向GitHub资源服务器获取资源(以 user_info 为例)这一步使用模拟的 GET 请求,携带参数包括:

  • access_token :令牌
  • token_type :令牌类型

4. 输出结果

/**
 * GitHub回调方法
 * @param code 授权码
 * @param state 应与发送时一致
 * @author jitwxs
 * @since 2018/5/21 15:24
 */
@RequestMapping("/githubCallback")
public void githubCallback(String code, String state, HttpServletResponse response) throws Exception {
    // 验证state,如果不一致,可能被CSRF攻击
    if(!oauthService.checkState(state)) {
        throw new Exception("State验证失败");
    }
    // 2、向GitHub认证服务器申请令牌
    String url = "https://github.com/login/oauth/access_token";
    // 传递参数grant_type、code、redirect_uri、client_id
    String param = "grant_type=authorization_code&code=" + code + "&redirect_uri=" +
            GITHUB_REDIRECT_URL + "&client_id=" + GITHUB_CLIENT_ID + "&client_secret=" + GITHUB_CLIENT_SECRET;
    // 申请令牌,注意此处为post请求
    String result = HttpClientUtils.sendPostRequest(url, param);
    /*
     * result示例:
     * 失败:error=incorrect_client_credentials&error_description=The+client_id+and%2For+client_secret+passed+are+incorrect.&
     * error_uri=https%3A%2F%2Fdeveloper.github.com%2Fapps%2Fmanaging-oauth-apps%2Ftroubleshooting-oauth-app-access-token-request-errors%2F%23incorrect-client-credentials
     * 成功:access_token=7c76186067e20d6309654c2bcc1545e41bac9c61&scope=&token_type=bearer
     */
    Map<String, String> resultMap = HttpClientUtils.params2Map(result);
    // 如果返回的map中包含error,表示失败,错误原因存储在error_description
    if(resultMap.containsKey("error")) {
        throw  new Exception(resultMap.get("error_description"));
    }
    // 如果返回结果中包含access_token,表示成功
    if(!resultMap.containsKey("access_token")) {
        throw  new Exception("获取token失败");
    }
    // 得到token和token_type
    String accessToken = resultMap.get("access_token");
    String tokenType = resultMap.get("token_type");
    // 3、向资源服务器请求用户信息,携带access_token和tokenType
    String userUrl = "https://api.github.com/user";
    String userParam = "access_token=" + accessToken + "&token_type=" + tokenType;
    // 申请资源
    String userResult = HttpClientUtils.sendGetRequest(userUrl, userParam);
    // 4、输出用户信息
    response.setContentType("text/html;charset=utf-8");
    response.getWriter().write(userResult);
}

二、QQ 登录

2.1 注册应用

进入 QQ 互联管理中心:https://connect.qq.com/manage.html,创建一个新应用(需要先审核个人身份):

image.png

然后注册应用信息,和 GitHub 的步骤大差不差:

image.png

image.png

注册后,可以看到应用的 APP ID、APP Key,以及你被允许的接口,当然只有一个获取用户信息。

官方开发文档点击这里:http://wiki.connect.qq.com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side

注意:审核状态为审核中和审核失败也是可以使用的,不用担心(只是无法实际上线而已,作为 Demo 足够了)。

image.png

2.2 QQ 登录方法

private static String QQ_APP_ID = "101474821";
private static String QQ_APP_KEY = "00d91cc7f636d71faac8629d559f9fee";
private static String QQ_REDIRECT_URL = "http://127.0.0.1:8080/qqCallback";
@RequestMapping("/qqLogin")
public void qqLogin(HttpServletResponse response) throws Exception {
    // QQ认证服务器地址
    String url = "https://graph.qq.com/oauth2.0/authorize";
    // 生成并保存state,忽略该参数有可能导致CSRF攻击
    String state = oauthService.genState();
    // 传递参数response_type、client_id、state、redirect_uri
    String param = "response_type=code&" + "client_id=" + QQ_APP_ID + "&state=" + state
            + "&redirect_uri=" + QQ_REDIRECT_URL;
    // 1、请求QQ认证服务器
    response.sendRedirect(url + "?" + param);
}

2.3 QQ 回调方法

/**
 * QQ回调方法
 * @param code 授权码
 * @param state 应与发送时一致
 * @author jitwxs
 * @since 2018/5/21 15:24
 */
@RequestMapping("/qqCallback")
public void qqCallback(String code, String state, HttpServletResponse response) throws Exception {
    // 验证state,如果不一致,可能被CSRF攻击
    if(!oauthService.checkState(state)) {
        throw new Exception("State验证失败");
    }
    // 2、向QQ认证服务器申请令牌
    String url = "https://graph.qq.com/oauth2.0/token";
    // 传递参数grant_type、code、redirect_uri、client_id
    String param = "grant_type=authorization_code&code=" + code + "&redirect_uri=" +
            QQ_REDIRECT_URL + "&client_id=" + QQ_APP_ID + "&client_secret=" + QQ_APP_KEY;
    // 申请令牌,注意此处为post请求
    // QQ获取到的access token具有3个月有效期,用户再次登录时自动刷新。
    String result = HttpClientUtils.sendPostRequest(url, param);
    /*
     * result示例:
     * 成功:access_token=A24B37194E89A0DDF8DDFA7EF8D3E4F8&expires_in=7776000&refresh_token=BD36DADB0FE7B910B4C8BBE1A41F6783
     */
    Map<String, String> resultMap = HttpClientUtils.params2Map(result);
    // 如果返回结果中包含access_token,表示成功
    if(!resultMap.containsKey("access_token")) {
        throw  new Exception("获取token失败");
    }
    // 得到token
    String accessToken = resultMap.get("access_token");
    // 3、使用Access Token来获取用户的OpenID
    String meUrl = "https://graph.qq.com/oauth2.0/me";
    String meParams = "access_token=" + accessToken;
    String meResult = HttpClientUtils.sendGetRequest(meUrl, meParams);
    // 成功返回如下:callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
    // 取出openid
    String openid = getQQOpenid(meResult);
    // 4、使用Access Token以及OpenID来访问和修改用户数据
    String userInfoUrl = "https://graph.qq.com/user/get_user_info";
    String userInfoParam = "access_token=" + accessToken + "&oauth_consumer_key=" + QQ_APP_ID + "&openid=" + openid;
    String userInfo = HttpClientUtils.sendGetRequest(userInfoUrl, userInfoParam);
    // 5、输出用户信息
    response.setContentType("text/html;charset=utf-8");
    response.getWriter().write(userInfo);
}
/**
 * 提取Openid
 * @param str 形如:callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
 * @author jitwxs
 * @since 2018/5/22 21:37
 */
private String getQQOpenid(String str) {
    // 获取花括号内串
    String json = str.substring(str.indexOf("{"), str.indexOf("}") + 1);
    // 转为Map
    Map<String, String> map = JsonUtils.jsonToPojo(json, Map.class);
    return map.get("openid");
}

三、项目源码

QQ 登录的具体流程我就不啰嗦了,都差不多。代码只列出了关键方法,具体程序还包含工具类和 redis 的配置。具体请参考文章开头源码,该项目采用 SpringBoot 搭建,需要 Redis 支持。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
JSON 数据格式 Python
Python:OAuth2第三方登录之Github
Python:OAuth2第三方登录之Github
191 0
|
前端开发 测试技术 数据安全/隐私保护
|
1月前
|
人工智能 文字识别 异构计算
关于github开源ocr项目的疑问
小白尝试Python OCR学习,遇到报错。尝试Paddle OCR部署失败,Tesseract OCR在Colab误操作后恢复失败。EasyOCR在Colab和阿里天池Notebook成功,但GPU资源不足。其他平台部署不顺,决定使用WebUI或阿里云轻应用。求教OCR项目部署到本地及简单OCR项目推荐。
29 2
|
1月前
|
人工智能 自然语言处理 iOS开发
『GitHub项目圈选19』推荐5款本周 让人爱不释手 的开源项目
『GitHub项目圈选19』推荐5款本周 让人爱不释手 的开源项目
|
1月前
|
存储 Web App开发 人工智能
『GitHub项目圈选18』推荐5款本周 超实用 的开源项目
『GitHub项目圈选18』推荐5款本周 超实用 的开源项目
|
1月前
|
人工智能 物联网 机器人
『GitHub项目圈选17』推荐5款本周 火火火 的AI开源项目
『GitHub项目圈选17』推荐5款本周 火火火 的AI开源项目
193 1
|
1月前
|
JSON 搜索推荐 程序员
『GitHub项目圈选15』推荐5款本周 深受程序员喜爱 的开源项目
『GitHub项目圈选15』推荐5款本周 深受程序员喜爱 的开源项目
|
1月前
|
人工智能 自然语言处理 NoSQL
『GitHub项目圈选13』推荐5款本周 让人爱不释手 的开源项目
『GitHub项目圈选13』推荐5款本周 让人爱不释手 的开源项目
|
1月前
|
SQL NoSQL Linux
『GitHub项目圈选11』推荐5款本周 深受开发人员青睐 的开源项目
『GitHub项目圈选11』推荐5款本周 深受开发人员青睐 的开源项目