一、OAuth理论基础
1.1、什么是开放平台
在一些大型互联网公司,随着公司的业务发展逐渐庞大,需要和外部合伙伙伴进行合作,需要将公司的接口开放给外部其他合伙伙伴进行调用。
比如腾讯的QQ互联网(一键登录)、微信开放平台、蚂蚁金服开放平台 、微博开放平台,比如实现功能QQ联合登陆、微信扫码登陆。
还有就是在大型集团公司中,分为总公司,和旗下多个分公司,总公司与分公司相互通讯也可以采用开放平台形式对接口进行授权。
1.2、什么是OAuth2.0
(1)OAuth
OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问让他们存储在另外的服务提供者上的的信息,而不需要讲用户名和密码提供给第三方网站或者分享他们数据的所有内容。
(2)QQ登录OAuth2.0
对于用户相关的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等)。为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。采用OAuth2.0标准协议来进行用户身份验证和获取用户授权,相对于之前的OAuth1.0协议,其认证流程更简单和安全。
(3)在微服务中Oauth有两种形式(其实有4种):授权码模式、密码模式
(4)遵守OAuth2.0认证授权协议:JWT
(5)具体概念
- appId:商户号 永久不能改
- appKey:商户秘钥 这个是可以改的
- 授权码Code:获取accessToken,只有10分钟有效
- 回调地址:授权成功,重定向地址
- openid:开放平台生产唯一的用户id
1.3、OAuth授权原理
OAuth认证和授权的过程如下:
- 用户访问第三方网站,想对用户存放在服务商的某些资源进行操作。
- 第三方网站向服务商请求一个临时令牌。
- 服务商验证第三方网站的身份后,授予一个临时令牌。
- 第三方网站获得临时令牌后,将用户导向至服务商的授权页面请求用户授权,然后这个过程中将临时令牌和第三方网站的返回地址发送给服务商。
- 用户在服务商的授权页面上输入自己的用户名和密码,授权第三方网站访问所相应的资源。
- 授权成功后,服务商将用户导向第三方网站的返回地址。
- 第三方网站根据临时令牌从服务商那里获取访问令牌。
- 服务商根据令牌和用户的授权情况授予第三方网站访问令牌。
- 第三方网站使用获取到的访问令牌访问存放在服务商的对应的用户资源。
1.4、调用QQ互联步骤
- 生成授权链接,获取授权码
- 使用授权码获取AccessToken
- 使用AccessToken获取openId
- 使用openId获取用户信息
1.5、项目架构图
二、QQ联合登录代码实现
腾讯开放平台网址: https://connect.qq.com/index.html
腾讯开放平台文档: http://wiki.connect.qq.com/
代码实现思路:
1.编写授权链接接口
2.编写授权回调接口
### 获取到授权码
### 使用授权码获取accessToken
### 使用accessToken获取用户openid
3.使用openid查询数据库user信息表中是否有关联
授权链接:
https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101462456&redirect_uri=http://shop.mayikt.com/qqLoginBack&state=1
&
将腾讯开放平台Sdk4J放入本地Maven私服仓库中:
mvn install:install-file -Dfile=jar包的位置(参数一) -DgroupId=groupId(参数二) -DartifactId=artifactId(参数三) -Dversion=version(参数四) -Dpackaging=jar
mvn install:install-file -Dfile="E:\Sdk4J.jar" -DgroupId=com.tengxun -DartifactId=sdk4j -Dversion=1.0 -Dpackaging=jar
(1)生成授权链接
@Controller
public class QQAuthoriController extends BaseWebController {
@Autowired
private QQAuthoriFeign qqAuthoriFeign;
private static final String MB_QQ_QQLOGIN = "member/qqlogin";
@Autowired
private MemberLoginServiceFeign memberLoginServiceFeign;
/**
* 重定向到首页
*/
private static final String REDIRECT_INDEX = "redirect:/";
/**
* 生成QQ授权回调地址
*
* @return
*/
@RequestMapping("/qqAuth")
public String qqAuth(HttpServletRequest request) {
try {
String authorizeURL = new Oauth().getAuthorizeURL(request);
return "redirect:" + authorizeURL;
} catch (Exception e) {
return ERROR_500_FTL;
}
}
(2)QQ授权回调
@RequestMapping("/qqLoginBack")
public String qqLoginBack(HttpServletRequest request, HttpServletResponse response, HttpSession httpSession) {
try {
AccessToken accessTokenObj = (new Oauth()).getAccessTokenByRequest(request);
if (accessTokenObj == null) {
return ERROR_500_FTL;
}
String accessToken = accessTokenObj.getAccessToken();
if (StringUtils.isEmpty(accessToken)) {
return ERROR_500_FTL;
}
// 获取用户openid
OpenID openIDObj = new OpenID(accessToken);
String openId = openIDObj.getUserOpenID();
if (StringUtils.isEmpty(openId)) {
return ERROR_500_FTL;
}
BaseResponse<JSONObject> findByOpenId = qqAuthoriFeign.findByOpenId(openId);
if (!isSuccess(findByOpenId)) {
return ERROR_500_FTL;
}
Integer resultCode = findByOpenId.getCode();
// 如果使用openid没有查询到用户信息,则跳转到绑定用户信息页面
if (resultCode.equals(Constants.HTTP_RES_CODE_NOTUSER_203)) {
// 使用openid获取用户信息
UserInfo qzoneUserInfo = new UserInfo(accessToken, openId);
UserInfoBean userInfoBean = qzoneUserInfo.getUserInfo();
if (userInfoBean == null) {
return ERROR_500_FTL;
}
String avatarURL100 = userInfoBean.getAvatar().getAvatarURL100();
// 返回用户头像页面展示
request.setAttribute("avatarURL100", avatarURL100);
httpSession.setAttribute(WebConstants.LOGIN_QQ_OPENID, openId);
return MB_QQ_QQLOGIN;
}
// 自动实现登陆
JSONObject data = findByOpenId.getData();
String token = data.getString("token");
CookieUtils.setCookie(request, response, WebConstants.LOGIN_TOKEN_COOKIENAME, token);
return REDIRECT_INDEX;
} catch (Exception e) {
return ERROR_500_FTL;
}
}
(3)QQ绑定请求
@RequestMapping("/qqJointLogin")
public String qqJointLogin(@ModelAttribute("loginVo") LoginVo loginVo, Model model, HttpServletRequest request,
HttpServletResponse response, HttpSession httpSession) {
// 1.获取用户openid
String qqOpenId = (String) httpSession.getAttribute(WebConstants.LOGIN_QQ_OPENID);
if (StringUtils.isEmpty(qqOpenId)) {
return ERROR_500_FTL;
}
// 2.将vo转换dto调用会员登陆接口
UserLoginInpDTO userLoginInpDTO = MeiteBeanUtils.voToDto(loginVo, UserLoginInpDTO.class);
userLoginInpDTO.setQqOpenId(qqOpenId);
userLoginInpDTO.setLoginType(Constants.MEMBER_LOGIN_TYPE_PC);
String info = webBrowserInfo(request);
userLoginInpDTO.setDeviceInfor(info);
BaseResponse<JSONObject> login = memberLoginServiceFeign.login(userLoginInpDTO);
if (!isSuccess(login)) {
setErrorMsg(model, login.getMsg());
return MB_QQ_QQLOGIN;
}
// 3.登陆成功之后如何处理 保持会话信息 将token存入到cookie 里面 首页读取cookietoken 查询用户信息返回到页面展示
JSONObject data = login.getData();
String token = data.getString("token");
CookieUtils.setCookie(request, response, WebConstants.LOGIN_TOKEN_COOKIENAME, token);
return REDIRECT_INDEX;
}
(4)会员服务提供接口:
根据 openid查询是否已经绑定,如果已经绑定,则直接实现自动登陆
public interface QQAuthoriService {
/**
* 根据 openid查询是否已经绑定,如果已经绑定,则直接实现自动登陆
*
* @param openid
* @return
*/
@RequestMapping("/findByOpenId")
BaseResponse<JSONObject> findByOpenId(@RequestParam("qqOpenId") String qqOpenId);
}
@RestController
public class QQAuthoriServiceImpl extends BaseApiService<JSONObject> implements QQAuthoriService {
@Autowired
private UserMapper userMapper;
@Autowired
private GenerateToken generateToken;
@Override
public BaseResponse<JSONObject> findByOpenId(String qqOpenId) {
if (StringUtils.isEmpty(qqOpenId)) {
return setResultError("qqOpenId不能为空!");
}
// 1.根据openid查询用户信息
UserDo userDo = userMapper.findByOpenId(qqOpenId);
if (userDo == null) {
return setResultError(Constants.HTTP_RES_CODE_NOTUSER_203, "根据qqOpenId没有查询到用户信息");
}
// 2.如果能够查询到用户信息,则直接生成对应的用户令牌
String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + Constants.HTTP_RES_CODE_QQ_LOGINTYPE;
Long userId = userDo.getUserId();
String userToken = generateToken.createToken(keyPrefix, userId + "");
JSONObject data = new JSONObject();
data.put("token", userToken);
return setResultSuccess(data);
}
}