服务端开发流程文档
https://wiki.connect.qq.com/%e5%bc%80%e5%8f%91%e6%94%bb%e7%95%a5_server-side

  1. 开发者注册
  2. 放置QQ登录按钮
  3. 获取Access Token
  4. 获取用户的OpenID
  5. 调用OpenAPI访问修改用户信息

image.png

  1. 进入 /login页面,点击 QQ登录

    1. export const qqAuthUrl = 'https://graph.qq.com/oauth2.0/authorize';
    2. export const qqAuthOptions = {
    3. response_type: 'code',
    4. client_id: appId,
    5. redirect_uri,
    6. state,
    7. scope: 'get_user_info,list_album', // 获取的权限
    8. }
  2. 跳转到 QQ的授权页面

  3. 输入用户名和密码后,callbackUrl 返回的 code&state 10分内过期

    1. http://localhost:7001/user/callback?code=0B70FD281699B4085A572695777F5DAD&state=1638614181739
  4. 用 code去获取 accessToken

    1. {
    2. access_token: '7EE2C16CE8515D181132F3F8B160536C',
    3. expires_in: '7776000',
    4. refresh_token: '59D3B59675D6A25CB403B9399F0CF065'
    5. }
  5. fetchMe,用 code 和 accessToken获取 qq的 openid

    1. {"client_id":"101499238","openid":"F98*****************************"}
  6. 用 openid + access_token 获取用户的信息

QQ登录流程

https://wiki.connect.qq.com/%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c_oauth2-0
image.png

1 放置 QQ登录按钮

https://wiki.connect.qq.com/%e6%94%be%e7%bd%aeqq%e7%99%bb%e5%bd%95%e6%8c%89%e9%92%ae_oauth2-0
进入 /login页面,点击 QQ登录
image.png

  1. https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101423631&redirect_uri=https://account.geekbang.org/account/oauth/callback?type=qq&ident=062771&login=0&cip=0&redirect=https%3A%2F%2Faccount.geekbang.org%2Fthirdlogin%3Fremember%3D1%26type%3Dqq%26is_bind%3D0%26platform%3Dtime%26redirect%3Dhttps%253A%252F%252Ftime.geekbang.org%252F%26failedurl%3Dhttps%3A%2F%2Faccount.geekbang.org%2Fsignin%3Fredirect%3Dhttps%253A%252F%252Ftime.geekbang.org%252F&scope=get_user_info&response_type=code&state=52ff2fbc282fb13d02da9f0f95b576c1c8bf8f5c

2 跳转到 QQ授权页面

image.png

  1. // url 三次解码
  2. https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101423631&redirect_uri=https%3A%2F%2Faccount.geekbang.org%2Faccount%2Foauth%2Fcallback%3Ftype%3Dqq%26ident%3Da48ff0%26login%3D0%26cip%3D0%26redirect%3Dhttps%253A%252F%252Faccount.geekbang.org%252Fthirdlogin%253Fremember%253D1%2526type%253Dqq%2526is_bind%253D0%2526platform%253Dtime%2526redirect%253Dhttps%25253A%25252F%25252Ftime.geekbang.org%25252F%2526failedurl%253Dhttps%253A%252F%252Faccount.geekbang.org%252Fsignin%253Fredirect%253Dhttps%25253A%25252F%25252Ftime.geekbang.org%25252F&scope=get_user_info&response_type=code&state=83fafc298feeb4a77256c01cb56ccb900944c2db
  3. // 第一次解码
  4. decodeURIComponent(url)
  5. https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101423631&redirect_uri=https://account.geekbang.org/account/oauth/callback?type=qq&ident=062771&login=0&cip=0&redirect=https%3A%2F%2Faccount.geekbang.org%2Fthirdlogin%3Fremember%3D1%26type%3Dqq%26is_bind%3D0%26platform%3Dtime%26redirect%3Dhttps%253A%252F%252Ftime.geekbang.org%252F%26failedurl%3Dhttps%3A%2F%2Faccount.geekbang.org%2Fsignin%3Fredirect%3Dhttps%253A%252F%252Ftime.geekbang.org%252F&scope=get_user_info&response_type=code&state=52ff2fbc282fb13d02da9f0f95b576c1c8bf8f5c
  6. // 第二次解码
  7. decodeURIComponent(redirect_uri)
  8. https://account.geekbang.org/account/oauth/callback?type=qq&ident=062771&login=0&cip=0&redirect=https://account.geekbang.org/thirdlogin?remember=1&type=qq&is_bind=0&platform=time&redirect=https%3A%2F%2Ftime.geekbang.org%2F&failedurl=https://account.geekbang.org/signin?redirect=https%3A%2F%2Ftime.geekbang.org%2F&scope=get_user_info&response_type=code&state=52ff2fbc282fb13d02da9f0f95b576c1c8bf8f5c
  9. // 第三次解码
  10. decodeURIComponent(redirect)
  11. https://time.geekbang.org/&failedurl=https://account.geekbang.org/signin?redirect=https://time.geekbang.org/&scope=get_user_info&response_type=code&state=52ff2fbc282fb13d02da9f0f95b576c1c8bf8f5c

Authorization Code

PC网站:https://graph.qq.com/oauth2.0/authorize
请求方法:GET
https://wiki.connect.qq.com/%e4%bd%bf%e7%94%a8authorization_code%e8%8e%b7%e5%8f%96access_token

3 code授权码

输入QQ账号和密码后,返回 callback的 URL,并返回 code, state
code授权码只能用一次,用完销毁,再用报错

第一次重定向是获取 code 第二次重定向是获取 token

image.png
文档 https://wiki.connect.qq.com/%e4%bd%bf%e7%94%a8authorization_code%e8%8e%b7%e5%8f%96access_token
image.png

4 拿到授权码去获取 token

image.png

5 access_token获取 refresh_token

image.png

6 通过 access_token获取 openid

image.png

https://wiki.connect.qq.com/%e8%8e%b7%e5%8f%96%e7%94%a8%e6%88%b7openid_oauth2-0
image.png

7 通过 openid获取到用户的信息

https://graph.qq.com/user/get_user_info
image.png

获取用户信息文档

https://wiki.connect.qq.com/get_user_info
image.png

服务端代码

eggjs服务端代码

login.js

Controller/login.js

  1. 'use strict';
  2. const { Controller } = require('egg');
  3. const querystring = require('querystring');
  4. const axios = require('axios');
  5. const appId = '申请的 appId';
  6. const appKey = '申请的 appKey';
  7. const authorizeUrl = 'https://graph.qq.com/oauth2.0/authorize?';
  8. // 输入用户名和密码后,会跳转到回调地址
  9. const redirect_uri = 'http://front.zhufengpeixun.cn/user/callback';
  10. const state = Date.now();
  11. // 获取 token的 重定向
  12. const tokenUrl = 'https://graph.qq.com/oauth2.0/token?';
  13. const meUrl = 'https://graph.qq.com/oauth2.0/me?';
  14. const getUserInfo = 'https://graph.qq.com/user/get_user_info?';
  15. class LoginController extends Controller {
  16. async index() {
  17. const { ctx } = this;
  18. const options = {
  19. response_type: 'code',
  20. client_id: appId,
  21. redirect_uri,
  22. state,
  23. scope: 'get_user_info,list_album', // 获取的权限
  24. };
  25. const query = querystring.stringify(options);
  26. const ssoQQUrl = `${authorizeUrl}${query}`;
  27. const grantUrl = {
  28. wechat: 'https://wx.qq.com/',
  29. ding: 'https://im.dingtalk.com/',
  30. qq: ssoQQUrl,
  31. };
  32. await ctx.render('login.html', grantUrl);
  33. }
  34. async callback() {
  35. const { ctx } = this;
  36. console.log(10000000, ctx.query);
  37. // eslint-disable-next-line no-unused-vars
  38. const { code, state } = ctx.query;
  39. // ctx.body = {
  40. // status: 200,
  41. // code, state,
  42. // };
  43. // redirectUrl 1
  44. const options = {
  45. grant_type: 'authorization_code',
  46. client_id: appId,
  47. client_secret: appKey,
  48. code,
  49. redirect_uri,
  50. fmt: 'json',
  51. };
  52. const fetchTokenUrl = tokenUrl + querystring.stringify(options);
  53. const res = await axios.get(fetchTokenUrl);
  54. // ctx.body = {
  55. // status: 200,
  56. // code,
  57. // state,
  58. // data: res.data,
  59. // };
  60. // redirectUrl 2
  61. // eslint-disable-next-line no-unused-vars
  62. // const { access_token, expires_in, refresh_token } = querystring.parse(res.data);
  63. const { refresh_token } = res.data;
  64. const options2 = {
  65. grant_type: 'refresh_token',
  66. client_id: appId,
  67. client_secret: appKey,
  68. refresh_token,
  69. fmt: 'json',
  70. };
  71. const refreshTokenUrl = tokenUrl + querystring.stringify(options2);
  72. const res2 = await axios.get(refreshTokenUrl);
  73. // ctx.body = {
  74. // status: 200,
  75. // access_token, expires_in,
  76. // data: res2.data,
  77. // };
  78. // 通过 access_token 获取用户信息
  79. const { access_token } = res2.data;
  80. const options3 = {
  81. access_token,
  82. fmt: 'json',
  83. };
  84. const openidUrl = meUrl + querystring.stringify(options3);
  85. const res3 = await axios.get(openidUrl);
  86. // ctx.body = {
  87. // status: 200,
  88. // data: res3.data,
  89. // };
  90. // 通过 openid 获取用户信息
  91. const { openid } = res3.data;
  92. const options4 = {
  93. access_token,
  94. oauth_consumer_key: appId,
  95. openid,
  96. };
  97. const getUserUrl = getUserInfo + querystring.stringify(options4);
  98. const res5 = await axios.get(getUserUrl);
  99. ctx.body = {
  100. status: 200,
  101. data: res5.data,
  102. };
  103. }
  104. }
  105. module.exports = LoginController;

router.js

  1. 'use strict';
  2. module.exports = app => {
  3. const { router, controller } = app;
  4. // const userExist = app.middleware.userExist();
  5. router.get('/', controller.home.index);
  6. // SSO登录
  7. router.get('/login', controller.login.index);
  8. router.get('/user/callback', controller.login.callback);
  9. // 受保护的路由,必须登录才能访问
  10. // router.post('/order', userExist, controller.orders.order);
  11. router.get('/404', controller.error.notFound);
  12. };