跳到主要内容

QQ账号信任登录

概述

  1. QQ账号信任登录包含:PC网站应用登录和H5网站应用登录
  2. 每种登录方式都需要配置不同的应用ID和应用秘钥
  3. 总体逻辑为:发起登录 -> 用户授权 -> 获取用户信息 -> 根据用户信息进行登录

PC网站应用登录

逻辑概述

  1. 第三方应用(Javashop网站)发起QQ授权登录请求,QQ用户允许授权第三方应用(Javashop网站)后,微信会拉起应用或重定向到第三方网站(Javashop网站),并且带上授权临时票据code参数
  2. 通过code参数加上AppID和AppSecret等,通过API换取access_token
  3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作

详细逻辑可参考官方文档:网站应用接入概述

流程图

image-20230803174650122

重点步骤说明:

  • 步骤3:构建的授权URL格式如下:

    https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=APPID&redirect_uri=REDIRECT_URI&state=STATE

    response_type:授权类型,此值固定为“code”

    redirect_uri:成功授权后的回调地址,必须是注册appid时填写的主域名下的地址。注意需要将url进行URLEncode

    state:client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。程序中设置的是uuid

  • 步骤10-11:

    调用的接口为通过code获取access_token接口,如下:

    https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=AppID&client_secret=AppSecret&code=AuthorizationCode&redirect_uri=REDIRECT_URI

    接口返回的数据格式如下:

    默认是x-www-form-urlencoded格式

    access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14

    返回参数说明:

    参数描述
    access_token授权令牌,Access_Token
    expires_in该access token的有效期,单位为秒
    refresh_token在授权自动续期步骤中,获取新的Access_Token时需要提供的参数。
    注:refresh_token仅一次有效
  • 步骤12-13:

    调用的接口为获取用户openid接口,如下:

    https://graph.qq.com/oauth2.0/me?access_token=AccessToken&fmt=json

    接口返回的数据格式如下:

    {
    "client_id":"YOUR_APPID",
    "openid":"YOUR_OPENID"
    }
  • 步骤14:根据openid查询是否已经绑定注册了会员信息(查询es_connect表,此表存放的是第三方账号和系统会员的关联信息),如果未查询到数据,需要调用会员注册业务接口注册一条会员数据

  • 步骤15:会员登录后会创建会员token信息,由于是服务端进行的重定向跳转页面,因此跳转之前要将token信息存入cookie中方便前端读取

代码展示

Controller代码展示:

@Tag(name = "QQ统一登陆接口")
@RestController
@RequestMapping("/buyer/connect/qq")
public class LoginByQQController {

@Operation(summary = "PC发起信任登录")
@GetMapping("/pc/login")
public void pcLogin(){
//信任登录插件基类
AbstractConnectLoginPlugin connectionLogin = connectManager.getConnectionLogin(ConnectTypeEnum.QQ);
String loginUrl = connectionLogin.getLoginUrl(ClientTypeEnum.PC.name());
try {
ThreadContextHolder.getHttpResponse().sendRedirect(loginUrl);
} catch (IOException e) {
this.logger.error(e.getMessage(), e);
throw new ServiceException(MemberErrorCode.E131.name(), "联合登录失败");
}
}

}

构建授权登录URL代码展示:

@Component("qqConnectLoginPlugin")
public class QQConnectLoginPlugin extends AbstractConnectLoginPlugin {

/**
* 获取授权登录的url
*
* @param clientTypeEnum 客户端类型 {@link ClientTypeEnum}
* @return
*/
@Override
public String getLoginUrl(String clientTypeEnum) {

Map map = initConnectSetting(ThirdPlatformEnum.QQ, ClientTypeEnum.valueOf(clientTypeEnum));

String uuid = UUID.randomUUID().toString();

String callBack = this.getCallBackUrl(ConnectTypeEnum.QQ.value(), clientTypeEnum);

String url = "https://graph.qq.com/oauth2.0/authorize?" +
"response_type=code" +
"&client_id=" + map.get("app_id") +
"&redirect_uri=" + callBack +
"&state=" + uuid;
return url;
}

}

回调获取QQ用户openid代码展示:

@Component("qqConnectLoginPlugin")
public class QQConnectLoginPlugin extends AbstractConnectLoginPlugin {

/**
* 登录成功后的回调方法
*
* @param client 客户端类型 {@link ClientTypeEnum}
* @return
*/
@Override
public Auth2Token loginCallback(String client) {
debugger.log("进入QQConnectLoginPlugin回调");

//获取PC端QQ账号信任登录配置参数信息
Map map = initConnectSetting(ThirdPlatformEnum.QQ, ClientTypeEnum.valueOf(client));

HttpServletRequest request = ThreadContextHolder.getHttpRequest();
//通过Authorization Code获取Access Token
String code = request.getParameter("code");
String redirectUri = this.getCallBackUrl(ConnectTypeEnum.QQ.value(), client);

//获取accessToken信息
String url = "https://graph.qq.com/oauth2.0/token?" +
"grant_type=authorization_code" +
"&client_id=" + map.get("app_id") +
"&client_secret=" + map.get("app_secret") +
"&code=" + code +
"&redirect_uri=" + redirectUri;

String content = HttpUtils.doGet(url, "UTF-8", 1000, 1000);
String accessToken = "";
Matcher matcher = accessTokenPattern.matcher(content);
while (matcher.find()) {
accessToken = matcher.group(1);
}

//获取QQ用户的openid
url = "https://graph.qq.com/oauth2.0/me?access_token=" + accessToken+"&unionid=1&fmt=json";
content = HttpUtils.doGet(url, "UTF-8", 1000, 1000);
String unionId = "";
Matcher unionIdMatcher = unionidPattern.matcher(content);
while (unionIdMatcher.find()) {
unionId = unionIdMatcher.group(1);
}

String openid = "";
Matcher openidMatcher = openidPattern.matcher(content);
while (openidMatcher.find()) {
openid = openidMatcher.group(1);
}

Auth2Token auth2Token = new Auth2Token();
auth2Token.setUnionid(unionId);
auth2Token.setOpneId(openid);
auth2Token.setAccessToken(accessToken);
return auth2Token;
}

}

H5网站应用登录

逻辑概述

  1. 前端引用腾讯提供的JS SDK的JavaScript文件,此文件封装了QQ登录流程与API列表中的所有OpenAPI调用方法
  2. 通过SDK中封装的方法拉取授权登录页面,用户确认跳转至回调页面后,获取access_token
  3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作

具体逻辑可参考官方文档:网站应用接入概述

流程图

image-20230804101715616

重点步骤说明:

  • 步骤5:由前端调用SDK中的方法发起授权登录请求,如下:

    QC.Login.showPopup({ 
    appId: that.qq_app_id,
    redirectURI: that.redirect_uri
    })

    appId是通过步骤2中调用API获取的

    redirectURI是由前端定义的页面地址,用户确认授权登录后会跳转到该地址

  • 步骤8:在步骤5跳转值回调页面时,回调地址上会携带access_token

  • 步骤10-11:

    调用的接口为获取用户openid接口,如下:

    https://graph.qq.com/oauth2.0/me?access_token=AccessToken&fmt=json

    接口返回的数据格式如下:

    {
    "client_id":"YOUR_APPID",
    "openid":"YOUR_OPENID"
    }
  • 步骤12-13:

    调用的接口为获取QQ用户信息接口,如下:

    https://graph.qq.com/user/get_user_info?
    access_token=AccessToken&oauth_consumer_key=AppID&openid=OpenId&format=json

    接口返回的数据格式如下:

    {
    "ret":0,
    "msg":"",
    "nickname":"Peter",
    "figureurl":"http://qzapp.qlogo.cn/qzapp/111111/942FEA70050EEAFBD4DCE2C1FC775E56/30",
    "figureurl_1":"http://qzapp.qlogo.cn/qzapp/111111/942FEA70050EEAFBD4DCE2C1FC775E56/50",
    "figureurl_2":"http://qzapp.qlogo.cn/qzapp/111111/942FEA70050EEAFBD4DCE2C1FC775E56/100",
    "figureurl_qq_1":"http://q.qlogo.cn/qqapp/100312990/DE1931D5330620DBD07FB4A5422917B6/40",
    "figureurl_qq_2":"http://q.qlogo.cn/qqapp/100312990/DE1931D5330620DBD07FB4A5422917B6/100",
    "gender":"男"
    }

    返回参数说明:

    image-20230804115600715

代码展示

Controller代码展示:

@Tag(name = "QQ统一登陆接口")
@RestController
@RequestMapping("/buyer/connect/qq")
public class LoginByQQController {

@Operation(summary = "获取appid")
@GetMapping("/h5/getAppid")
public String getAppId(){
return loginQQManager.getAppid();
}

@Operation(summary = "H5登陆")
@GetMapping("/h5/login")
public Map h5Login(String access_token, String uuid){
return loginQQManager.qqWapLogin(access_token,uuid);
}

}

业务接口代码展示:

@Service
public class LoginQQManagerImpl implements LoginQQManager {

/**
* 获取H5端QQ信息登录参数appid
* @return
*/
@Override
public String getAppid() {
Map<String, String> map = this.thirdPlatformAppManager.getConfigOnConnect(ThirdPlatformEnum.QQ, ClientTypeEnum.H5);
return map.get("app_id");
}

/**
* H5端QQ账号信任登录
*
* @param accessToken 接口调用凭证
* @param uuid 本次请求唯一标识
* @return
*/
@Override
public Map qqWapLogin(String accessToken, String uuid) {
LoginUserDTO loginUserDTO = new LoginUserDTO();
loginUserDTO = getUnionInfo(loginUserDTO,accessToken);
loginUserDTO.setUuid(uuid);
loginUserDTO.setTokenOutTime(null);
loginUserDTO.setRefreshTokenOutTime(null);
loginUserDTO.setOpenType(ConnectTypeEnum.QQ_OPENID);
loginUserDTO.setUnionType(ConnectTypeEnum.QQ);
loginUserDTO = getQQUserInfo(loginUserDTO,accessToken);
return loginManager.loginByUnionId(loginUserDTO);
}

/**
* 获取用户openid信息
*
* @param loginUserDTO 登录用户信息
* @param accessToken 接口调用凭证
* @return
*/
private LoginUserDTO getUnionInfo(LoginUserDTO loginUserDTO,String accessToken){
StringBuffer unionIdBuffer = new StringBuffer("https://graph.qq.com/oauth2.0/me?");
unionIdBuffer.append("access_token=").append(accessToken);
unionIdBuffer.append("&unionid=1&fmt=json");
String retJson = HttpUtils.doGet(unionIdBuffer.toString(), "UTF-8", 1000, 1000);
if (retJson.indexOf("unionid")==-1){
throw new ServiceException("403","fail to getById unionid",retJson);
}
JSONObject jsonObject = JSONObject.fromObject(retJson);
loginUserDTO.setUnionid(jsonObject.getString("unionid"));
loginUserDTO.setOpenid(jsonObject.getString("openid"));
return loginUserDTO;
}

/**
* 获取QQ用户信息
*
* @param loginUserDTO 登录用户信息
* @param accessToken 接口调用凭证
* @return
*/
private LoginUserDTO getQQUserInfo(LoginUserDTO loginUserDTO, String accessToken){
Map<String, String> map = this.thirdPlatformAppManager.getConfigOnConnect(ThirdPlatformEnum.QQ, ClientTypeEnum.H5);
StringBuffer userBuffer = new StringBuffer("https://graph.qq.com/user/get_user_info?");
userBuffer.append("access_token=").append(accessToken);
userBuffer.append("&openid=").append(loginUserDTO.getOpenid());
userBuffer.append("&oauth_consumer_key=").append(map.get("app_id"));
userBuffer.append("&format=json");
String retJson = HttpUtils.doGet(userBuffer.toString(), "UTF-8", 1000, 1000);
JSONObject jsonObject = JSONObject.fromObject(retJson);
if (jsonObject.getInt("ret")!=0){
throw new ServiceException("403","获取用户信息失败",retJson);
}
loginUserDTO.setHeadimgurl(jsonObject.getString("figureurl_qq"));
loginUserDTO.setNickName(jsonObject.getString("nickname"));
if ("男".equals(jsonObject.getString("gender"))){
loginUserDTO.setSex(1);
}else{
loginUserDTO.setSex(0);
}
loginUserDTO.setProvince(jsonObject.getString("province"));
loginUserDTO.setCity(jsonObject.getString("city"));
return loginUserDTO;
}
}