前言
OAuth2.0非常复杂,有很多的内容,我觉得这个偏向于安全领域,所以我作为一个后端就做一个简单的了解,不会深入,主要还是体现QQ的第三方登录的实现。
OAuth2.0
OAuth全称Open Authentication(官方文章,内容过于多,不便于阅读,还是看别人总结的文章比较好),简单来说就是做第三方登录的,就是我们常用的微信登录这些。
开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
下图为OAuth2.0的抽象流程图(Abstract Protocol Flow):
Client:客户端(用户)
Resource Owner:资源拥有者(QQ客户端)
Authorization Server:授权服务器(QQ授权中心)
Resource Server:资源服务器(QQ用户信息库)
Authorization Request:授权请求(请求QQ要通过微信登录CSDN)
Authorization Grant:授权许可(QQ返回一个授权证明(可以是个授权码))
Access Token:访问令牌(QQ返回一个Token令牌)
Protected Resource:被保护资源(QQ返回被需要的资源,头像、姓名等)
这里有一个问题,为什么有了Authorization Grant还要Access Token?
这个问题感觉还挺复杂的,主要涉及了很多的安全问题,不深究了,可以参考以下网站。
- https://stackoverflow.com/questions/13387698/why-is-there-an-authorization-code-flow-in-oauth2-when-implicit-flow-works-s
- https://www.cnblogs.com/erichi101/p/13497971.html
- https://www.zhihu.com/question/275041157
1, 认证服务器的返回是通过重定向实现的,我理解重定向只能在url上做文章,意味着所有信息都只能通过url传输。通过传输code,让应用再去请求一次token是为了不直接暴露token。code一般都是只能在很短的时间内有效,并且只能使用一次。保证了安全性。
2,你说的直接通过认证服务器将code传给后端接口,这个肯定不行,因为认证服务器调了你的接口,你自己决定重定向的url,但是重定向的请求没有返回给用户,而是给了认证服务器,有什么用呢?
作者:ericbu
链接:https://www.zhihu.com/question/275041157/answer/2009095840
来源:知乎
简单来讲服务器与服务器之间的通信是高度安全的,浏览器/移动 app与服务器的通信是安全性较低的。这张Abstract图并没有体现出来,结合下面的图可以更清晰一些。
我一直不理解的是,为什么token不直接给了服务器,而要通过先把code传回浏览器,在携带code发一个请求,根据上面这些内容,除了安全的考虑,还有就是一点,重定向必须走浏览器,因为为了要刷新浏览器,你如果直接把token给了网站后台,浏览器不发起一个新的请求,页面也是不会改变的。而且可以看到下面“1.2重定向至授权服务”、“4.3携带code重定向至网站服务器”它们是指向浏览器的,刚开始这个指向我还故意改成了指向授权服务、网站服务,这是错误的,就是应该指向浏览器。
上面是一个大概的流程,结合下面这个图(图片来自:YouTube),我稍微修改了一下,应该可以更好的理解。
这里是官网的图(授权码模式,上面呢个只是抽象图,不是具体的实现),对应上面的过程。总的来说就是两步:
- 获取Authorization Code;
- 通过Authorization Code获取Access Token
看下面这个图,在Dcard里使用Google登录时浏览器会发起一个新的URL请求。同意之后,Dcard的页面会进行刷新,也就是浏览器又发起一个新的请求,差不多符合上面的逻辑。
上面这个是授权码模式(Authorization Code)是 OAuth 功能最齐全、流程最严谨,也是最常用的授权模式。此外OAuth2.0共定义了4种授权方式(详情请看:https://www.cnblogs.com/alittlesmile/p/11531577.html):
- 隐式授权模式:在redirect_url中传递access_token,oauth客户端运行在浏览器中。
- 授权码授权模式:通过客户端的后台服务器,向服务端认证。
- 密码模式:将用户名和密码传过去,直接获取access_token。
- 客户端凭证模式:用户向客户端注册,然后客户端以自己的名义向“服务端”获取资源。
QQ第三方登录实现
结合代码,具体实现一下QQ的第三方登录,可能会对上面提到的内容更加理解。
QQ的第三方登录实现起来还是比较简单的。首先进入QQ开放平台,就按顺序填写即可,比较清晰,可以按照BiliBili这里的视频。需要注意的是你需要有自己的域名,还有备案号。还有一个是回调地址写什么,可以参考官方的说明,其实没太多要求随便写一个就行,我就直接在域名后面加了个callback。
在申请接入时,有两个个注意点,满足这两点才会给你通过:
- 你得有备案的域名,当然还要绑定好你的服务器。
- 申请后他们会查看你提供的域名,你需要成功部署你的网站,还得加入QQ登录的按钮,并且能够跳出QQ登录页面。
前言
具体在说明一下。
这里对应的就是网站地址,你要给负责审核人员看这个。
点击“QQ登录”后,跳出QQ登录页面(在没有通过审核时,这里不会出现二维码,会出现一串字,大概是说没有通过审核)(怎么跳到这里下面会写)。
只有这样,QQ管理员才会通过你的审核,才能之后出现这个能扫码的东西。
具体实现
前端我给大家准备好了,这个样式是我从别的地方随便复制过来的,自己加了点JS。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
height: 100%;
}
.container {
height: 100%;
background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);
}
.login-wrapper {
background-color: #fff;
width: 358px;
height: 588px;
border-radius: 15px;
padding: 0 50px;
position: relative;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.header {
font-size: 38px;
font-weight: bold;
text-align: center;
line-height: 200px;
}
.input-item {
display: block;
width: 100%;
margin-bottom: 20px;
border: 0;
padding: 10px;
border-bottom: 1px solid rgb(128, 125, 125);
font-size: 15px;
outline: none;
}
.input-item:placeholder {
text-transform: uppercase;
}
.btn {
text-align: center;
padding: 10px;
width: 100%;
margin-top: 40px;
background-image: linear-gradient(to right, #a6c1ee, #fbc2eb);
color: #fff;
}
.msg {
text-align: center;
line-height: 88px;
}
a {
text-decoration-line: none;
color: #abc1ee;
}
</style>
</head>
<body>
<div class="container">
<div class="login-wrapper">
<div class="header">Login</div>
<div class="form-wrapper">
<input type="text" name="username" placeholder="用户名" class="input-item" id="el1">
<input type="password" name="password" placeholder="密码" class="input-item" id="el2">
<div><button class="btn" onclick="method1()">登录</button></div>
<div><button class="btn" onclick="method2()">QQ登录</button></div>
</div>
<div class="msg">
没有账户?
<a href="#" onclick="method3()">注册</a>
</div>
</div>
</div>
<script>
function method1() {
alert("账号或密码错误");
document.getElementById('el1').value=''
document.getElementById('el2').value=''
}
function method2() {
// 像后端发起请求,拿到QQ给你的跳转URL
fetch("/login/QQLogin", {
method: "post",
}).then(function (r) {
return r.json()
}).then((r) => {
// 前端去跳这个URL
window.open(r.data, "_self");
console.log(r);
})
}
function method3() {
alert("目前不接受新人用户");
}
</script>
</body>
</html>
之后就是后端代码,首先引入依赖。
<!-- QQ -->
<dependency>
<groupId>net.gplatform</groupId>
<artifactId>Sdk4J</artifactId>
<version>2.0</version>
</dependency>
先建一个配置文件qqconnectconfig.properties
,只有三个地方需要改,呢几个在QQ互联的页面上。
app_ID = 填你的
app_KEY = 填你的
redirect_URI = 填你的
scope = get_user_info,add_topic,add_one_blog,add_album,upload_pic,list_album,add_share,check_page_fans,add_t,add_pic_t,del_t,get_repost_list,get_info,get_other_info,get_fanslist,get_idollist,add_idol,del_ido,get_tenpay_addr
baseURL = https://graph.qq.com/
getUserInfoURL = https://graph.qq.com/user/get_user_info
accessTokenURL = https://graph.qq.com/oauth2.0/token
authorizeURL = https://graph.qq.com/oauth2.0/authorize
getOpenIDURL = https://graph.qq.com/oauth2.0/me
addTopicURL = https://graph.qq.com/shuoshuo/add_topic
addBlogURL = https://graph.qq.com/blog/add_one_blog
addAlbumURL = https://graph.qq.com/photo/add_album
uploadPicURL = https://graph.qq.com/photo/upload_pic
listAlbumURL = https://graph.qq.com/photo/list_album
addShareURL = https://graph.qq.com/share/add_share
checkPageFansURL = https://graph.qq.com/user/check_page_fans
addTURL = https://graph.qq.com/t/add_t
addPicTURL = https://graph.qq.com/t/add_pic_t
delTURL = https://graph.qq.com/t/del_t
getWeiboUserInfoURL = https://graph.qq.com/user/get_info
getWeiboOtherUserInfoURL = https://graph.qq.com/user/get_other_info
getFansListURL = https://graph.qq.com/relation/get_fanslist
getIdolsListURL = https://graph.qq.com/relation/get_idollist
addIdolURL = https://graph.qq.com/relation/add_idol
delIdolURL = https://graph.qq.com/relation/del_idol
getTenpayAddrURL = https://graph.qq.com/cft_info/get_tenpay_addr
getRepostListURL = https://graph.qq.com/t/get_repost_list
version = 2.0.0.0
之后写获得跳转URL的接口,下面这个就是前端点击“QQ登录”触发的接口,authorizeUrl
返回给前端,让它打开,对应前端的method2()
方法。如果不是前后端分离,直接让后端打开就行。
@PostMapping("/QQLogin")
public JsonResult QQLogin(HttpServletRequest request) throws QQConnectException {
// 获取授权链接
String authorizeUrl = new Oauth().getAuthorizeURL(request);
// 重定向到授权链接
System.out.println(authorizeUrl);
return new JsonResult<>(authorizeUrl, "获取QQ登录链接成功");
}
最后写好回调方法,地址对应你在QQ互联上填的地址。这里主要就是拿到QQ的openid
,要用这个来区分用户。拿到之后,你就可以自由发挥了。
@GetMapping("/callback") // 必须GET,因为QQ要回调,它要走GET请求
public String QQLoginCallback(HttpServletRequest request) throws QQConnectException {
// 通过回调中的code得到accessToken
AccessToken accessTokenObj = new Oauth().getAccessTokenByRequest(request);
String accessToken = accessTokenObj.getAccessToken();
if(accessToken == null || "".equals(accessToken)){
throw new QQConnectException("accessToken为空,授权失败");
}
// 通过accessToken得到openId
OpenID openIdObj = new OpenID(accessToken);
if(openIdObj == null || "".equals(openIdObj.getUserOpenID())){
throw new QQConnectException("openIdObj为空,授权失败");
}
String openId = openIdObj.getUserOpenID();
// 通过accessToken和openId得到用户信息
UserInfo qzoneUserInfo = new UserInfo(accessToken, openId);
UserInfoBean userInfoBean = qzoneUserInfo.getUserInfo();
if(userInfoBean == null || userInfoBean.getRet() != 0){
throw new QQConnectException(userInfoBean.getMsg()+",授权失败");
}
//得到用户昵称
String nickname = userInfoBean.getNickname();
String imgUrl = userInfoBean.getAvatar().getAvatarURL30();
System.out.println(openId);
System.out.println(nickname);
System.out.println(imgUrl);
String result = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>qq login</title></head><body>qq昵称:"+nickname+"<br>qq头像:<img src='"+imgUrl+"'/></body></html>";
return result;
}
总结
QQ登录OAuth2.0总体处理流程如下:
- 申请接入,获取appid和apikey;
- 开发应用,做好我上面说的,去提交审核;
- 审核通过后,跳转的页面可以扫二维码;
- 用户扫码,通过用户登录验证和授权,获取Access Token;
- 通过Access Token获取用户的OpenID;
- 调用OpenAPI,来请求访问或修改用户授权的资源。
其它
另外说一下这个“unionid”是啥,官方说的很清楚,如果只有一个应用就用“openid”就可以了。
此接口用于获取个人信息。开发者可通过openID来获取用户的基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用,可通过获取用户的unionID来区分用户的唯一性,因为只要是同一QQ互联平台下的不同应用,unionID是相同的。换句话说,同一用户,对同一个QQ互联平台下的不同应用,unionID是相同的。
钉钉第三方登录
钉钉的也顺便实现了,具体去看钉钉开发平台的文章的文章就可以了。与QQ有点不一样吧。下面是获取openId的方法(核心其实就是拿到用户的openId)。
前端跳转扫码页面。
function method4() {
// 与QQ有点不同,QQ需要后端请求返回一个URL,而钉钉是固定的
window.open("https://login.dingtalk.com/oauth2/auth?redirect_uri=你的&response_type=code&client_id=你的&scope=openid&prompt=consent", "_self");
}
回调方法。
@GetMapping("/callback2")
public JsonResult callbackForDingDing(@RequestParam(value = "authCode")String authCode, HttpSession session) {
String openId = commonService.getDingDingID(authCode);
// ...你的逻辑
}
获取openId的方法,比QQ复杂些,另外写了个类
public String getDingDingID(String authCode) {
com.aliyun.dingtalkoauth2_1_0.Client client = null;
try {
client = authClient();
} catch (Exception e) {
throw new RuntimeException(e);
}
GetUserTokenRequest getUserTokenRequest = new GetUserTokenRequest()
//应用基础信息-应用信息的AppKey,请务必替换为开发的应用AppKey
.setClientId("你的")
//应用基础信息-应用信息的AppSecret,,请务必替换为开发的应用AppSecret
.setClientSecret("你的")
.setCode(authCode)
.setGrantType("authorization_code");
GetUserTokenResponse getUserTokenResponse = null;
try {
getUserTokenResponse = client.getUserToken(getUserTokenRequest);
} catch (Exception e) {
throw new RuntimeException(e);
}
//获取用户个人token
String accessToken = getUserTokenResponse.getBody().getAccessToken();
com.aliyun.dingtalkcontact_1_0.Client client2 = null;
try {
client2 = contactClient();
} catch (Exception e) {
throw new RuntimeException(e);
}
GetUserHeaders getUserHeaders = new GetUserHeaders();
getUserHeaders.xAcsDingtalkAccessToken = accessToken;
//获取用户个人信息,如需获取当前授权人的信息,unionId参数必须传me
String openId = null;
try {
openId = client2.getUserWithOptions("me", getUserHeaders, new RuntimeOptions()).getBody().getOpenId();
} catch (Exception e) {
throw new RuntimeException(e);
}
return openId;
}
坑
其他都还好,就是有一个坑,大家可以看我这篇文章文章。