之所以写这篇文章,是因为最近做的项目是根据oa2.0协议做的,不是简单使用别人的三方登录,而是做这样的一个授权平台去让别人去用。并且也有朋友问这个东西相关的内容,因此在此总结。
1.这是什么协议?
OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 2.0即完全废止了OAuth1.0。 OAuth 2.0关注客户端开发者的简易性。要么通过组织在资源拥有者和HTTP服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012年10月,OAuth 2.0协议正式发布为RFC 6749 (引用自百度百科) 。
2.协议为何存在?
本身来说是为了适应一种业务场景而实现出来的一种认证流程。
我们假设有着两个企业,企业A拥有着庞大的用户资源(像wx这种),企业B只拥有少数用户资源,对企业B来说,为了用户操作的便利性,而且用户本身也不想注册太多的账号,如果有一个账号可以通过多个平台,无非是一种爽事,但是这个账号的企业拥有者最好是拥有着比较大的用户资源。
对A来说,我拥有着这么庞大的用户资源,如果提供这样一种方式可以让我的用户可以直接去登录其他的平台,无非也是一种增加用户粘合性的方式。因此三方登录便随着时代的发展则萌生了。
不管是不是内部企业的平台还是外部企业的平台来说,这种都是有利的,一般来说,每家企业都会拥有多个不同的业务平台,比如对游戏公司来说,oa平台,客服平台,业务平台这些都是不可少的。如果每个业务平台都使用单独的账号系统,对使用者来说也是极为麻烦的,主要体现在我记不了那么多个账号和密码。而且,上面说的系统是属于公司内部系统的,那如果不是内部系统呢?多个平台多个账号,我们要如何对用户的行为进行统计呢?每个平台单独实现交互逻辑来交换数据?这样成本也太高了吧。一旦我们需要对用户进行行为分析,系统监控,根本无法进行下手。
此时,你应该明白了为何需要三方登录这种东西了吧。那么接下来,我们又要思考,怎么实现这种登录方式。
一般人会想到,那就一个用户数据库啊,然后每个平台都用同一套数据库啊,对啊,好像没毛病,但是这种只适用于内部系统的使用,那么有对外的应用怎么办,我不能让用户在其他公司的平台登录框填我这边的用户名和密码然后调我接口校验吧,这样的话调用方不就直接知道了用户的凭证,可以随意登录其他平台了,这种很明显不可取。我们需要一种方式,可以让调用方不晓得用户名的账号密码的情况下登录,而且我们只提供一些必要但是不重要的用户资源,例如头像,昵称,性别等。
此时,oauth2.0协议出来了,这里我们针对其授权码模式进行说明(这个协议是有四种模式的,而且安全性都不同,其中授权码模式是最为安全的),这种协议遵循在外部应用无法得知用户的账号密码的情况下,我们提供一个令牌作为用户资源的换取凭证,来间接的实现三方登陆。
3.2.0和1.0的区别
①auth1.0与Oauth2.0是相互不兼容的,所以他们为我们提供了不同的授权方式:
2.0的用户授权过程有3步:
A)用户到授权服务器,请求授权,然后返回授权码(AuthorizationCode)
B)客户端由授权码到授权服务器换取访问令牌(access token)
C)用访问令牌去访问得到授权的资源、
总结:获取授权码(Authorization Code)—>换取访问令牌(access_token)—>访问资源:
1.0的授权分4步,
A)客户端到授权服务器请求一个授权令牌(requesttoken&secret)
B)引导用户到授权服务器请求授权
C)用访问令牌到授权服务器换取访问令牌(accesstoken&secret)
D)用访问令牌去访问得到授权的资源
总结:请求授权令牌(request token&secret)—>换取访问令牌(access token&secret)—>访问资源
②1.0协议每个token都有一个加密,2.0则不需要。这样来看1.0似乎更加安全,但是2.0要求使用https协议,安全性也更高一筹。
③2.0充分考虑了客户端的各种子态,因而提供了多种途径获取访问令牌,有:授权码、
客户端私有证书、资源拥有者密码证书、刷新令牌等方式,而且验证过程更为简洁。
相比之下 1.0只有一个用户授权流程。
(两者区别内容引用自https://blog.csdn.net/u013436121/article/details/23631885)
4.协议的简述
这种认证协议主要是有着四种模式的。即下面四种:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
1)授权码模式
这是 一种最为安全的授权流程。主要的流程图如下:
需要以下四步:
第一步:跳转至授权页,用户选择是否登录或是否同意授权
第二步:授权完成后,业务系统获取到一次性授权码
第三步:使用授权码获取AccessToken令牌
第四步:使用令牌获取用户资源
2)简化模式
整个过程不通过三方服务器,在浏览器中完整实现。
第一步:跳转至授权页,用户选择是否登录或是否同意授权
第二步:若用户授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌
第三步:浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值
第四步:资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌
第五步:浏览器执行上一步所获得的脚本,用于提取出令牌
第六步:使用令牌获取用户资源
3)密码模式
使用账号密码来换取accessToken,一般用于内部高度可信业务。
第一步:用户向客户端提供用户名和密码
第二步:客户端将用户名和密码发给认证服务器,向后者请求令牌
第三步:认证服务器确认无误后,向客户端提供访问令牌
第四步:客户端用令牌获取用户资源
4)客户端模式
指客户端以自己的名义,而不以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题
第一步:客户端向认证服务器进行身份认证,并要求一个访问令牌。
第二步:认证服务器确认无误后,向客户端提供访问令牌。
5.如何采用这种协议调用三方登录
这里使用qq三方登录作为例子说明(因为这个用的人比较多):
1)去qq互联注册申请认证
2)创建应用获取到app_id,secret
3)根据授权码模式来取得用户资源
流程如下:
(1)获取code
(2)用code,加上app_id,secret,redirect_url换取accessToken
(3)用accessToken换取用户资源
以下是伪例代码,没有使用官方提供的SDK
以下是控制层代码:
<?php
namespace App\Http\Controllers\Oauth2;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
//用到的授权基础url
private static $QQ_URLS = [
'getCode'=> 'https://graph.qq.com/oauth2.0/authorize', //跳转的授权链接
'getAccessToken' => 'https://graph.qq.com/oauth2.0/token', //用code获取令牌
'getOpenId' => 'https://graph.qq.com/oauth2.0/me', //用令牌获取用户open_id
'getUserInfo' => 'https://graph.qq.com/user/get_user_info', //用open_id+access_token 换取用户信息
] ;
//回调地址,这里注意回调地址的域名需要和应用白名单设置那个回调域名一致
private static $REDIRECT_URLS = [
'code' => '', //跳转授权时的回调地址
'token' => '' //用code换取access_token的回调地址
];
//应用相关参数
private static $APP = [
'client_id' => '', //应用id
'client_secret' => '' //应用密钥
];
/**
* 跳转授权页主页面
* @author Jinzhong.lu
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
//调用的相关参数,一般来说,state在此应该存储下来,为下一步用code获取令牌校验使用
$params = [
'client_id' => self::$APP['client_id'],
'redirect_uri' => self::$REDIRECT_URLS['code'],
'state' => str_random(5)
];
//重定向的url
$url = self::$QQ_URLS['getCode'] ."?response_type=code&client_id=".$params['client_id'] . "&redirect_uri=" . $params['redirect_uri'] . "&state=" . $params['state'];
//判断是什么请求方式
if( $this->isGet()) {
//传递数据到视图
return view('oauth2/index',['data'=>$params,'url'=> $url]);
}else{
//进行重定向来拿code
header("Location: ".$url);exit();
}
}
/**
* 接收code,用code换令牌
* @author Jinzhong.lu
*/
public function getCode()
{
//从get参数中拿到code
$code = $_GET['code'];
$state = $_GET['state'];
//TODO 校验state 是否和上次传递的一样
//调用的参数
$params = [
'client_id' => self::$APP['client_id'],
'client_secret' => self::$APP['client_secret'],
'redirect_uri' => self::$REDIRECT_URLS['token'],
];
//重定向的url
$url = self::$QQ_URLS['getAccessToken'] ."?grant_type=authorization_code&client_id=" .$params['client_id']. "&client_secret=".$params['client_secret']."&redirect_uri=" .$params['redirect_uri']."&code=" .$code;
//判断是什么请求方式
if( $this->isGet()) {
//传递数据到视图
return view('oauth2/code',['data'=>$params,'url'=> $url]);
}else{
header("Location: ".$url);;exit();
}
}
/**
* 接收令牌,用令牌获取用户资源
* @author Jinzhong.lu
*/
public function getAccessToken(Request $request)
{
$params = [
'access_token' => $_GET['access_token'],
'expires_in' => $_GET['expires_in'],
'refresh_token' => $_GET['refresh_token']
];
return view('oauth2/token',['data'=>$params]);
}
/**
* 获得用户OpenId
* @author Jinzhong.lu
*/
public function getOpenId()
{
$access_token = $_GET['access_token'];
if (!$access_token) die('access_token不存在');
$url =self::$QQ_URLS['getOpenId']."?access_token=" . $access_token;
$res = $this->_get($url);
$data = [];
preg_match("/(?:\()(.*)(?:\))/i",$res, $data);
$str = trim(trim(trim($data[0],'('),')'),'');
$arr = json_decode($str,true);
//取出open_id,用open_id+client_id+access_token 获取用户的资源
$open_id = $arr['openid'];
$getInfoUrl = self::$QQ_URLS['getUserInfo']."?access_token=".$access_token ."&oauth_consumer_key=".self::$APP['client_id']."&openid=".$open_id;
$info = $this->_get($getInfoUrl);
print_r($info);
}
/**
* 用accessToken获取用户信息
* @author Jinzhong.lu
*/
public function getUserInfo()
{
$access_token = $_GET['access_token'];
if (!$access_token) die('access_token不存在');
$open_id = $_GET['open_id'];
if (!$open_id) die('open_id不存在');
$getInfoUrl = self::$QQ_URLS['getUserInfo']."?access_token=".$access_token ."&oauth_consumer_key=".self::$APP['client_id']."&openid=".$open_id;
$info = $this->_get($getInfoUrl);
print_r($info);
}
/**
* 封装post请求
* @param $url
* @param $params
* @param $timeout
* @param $headers
* @return bool|string
*/
private function _post($url, $params, $headers = [], $timeout = 5)
{
$curl = curl_init();
//设置抓取的url
curl_setopt($curl, CURLOPT_URL, $url);
//设置header
if ($headers !== []) {
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
}
//设置头文件的信息作为数据流输出
curl_setopt($curl, CURLOPT_HEADER, 1);
//设置获取的信息以文件流的形式返回,而不是直接输出。
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
//设置超时时间
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout);
//设置post方式提交
curl_setopt($curl, CURLOPT_POST, 1);
//设置post数据
curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
//执行命令
$data = curl_exec($curl);
//除去头信息部分内容
if (curl_getinfo($curl, CURLINFO_HTTP_CODE) == '200') {
list($header, $body) = explode("\r\n\r\n", $data, 2);
}
//关闭URL请求
curl_close($curl);
//显示获得的数据
var_dump($data);
die;
return $body;
}
/**
* GET请求
* @param $uri
* @param int $timeout
* @param string $cookie
* @return bool|string
* @throws UdbException
*/
private function _get($uri, $timeout = 5, $cookie = [])
{
$urlArray = parse_url($uri);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $uri);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
if ($cookie !== []) {
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
}
if ($urlArray['scheme'] == 'https') curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($ch);
$state = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errorMsg = null;
$errorCode = 0;
if ($state !== 200) {
$errorMsg = 'GET请求异常: ' . curl_error($ch);
$errorCode = curl_errno($ch);
}
curl_close($ch);
if (200 == $state) return $result;
static::curlThrow($errorMsg, $errorCode);
return false;
}
/**
* 判断是否为POST请求
* @return bool
*/
public function isPost()
{
return isset($_SERVER['REQUEST_METHOD']) && strtoupper($_SERVER['REQUEST_METHOD'])=='POST';
}
/**
* 判断是否为GET请求
* @return bool
*/
public function isGet()
{
return isset($_SERVER['REQUEST_METHOD']) && strtoupper($_SERVER['REQUEST_METHOD'])=='GET';
}
}
以下是三个视图,使用的是laravel的blade视图,因最近想学下这个框架,因此拿这个练下手。
index.blade.php
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
.main{
width: 800px;
height: 200px;
margin: 0 auto;
margin-top: 300px;
}
.params{
text-align: center;
}
.form{
margin-left: 280px;
}
</style>
<title>QQ三方登录</title>
</head>
<body>
<div class="main">
<div class="params">
<p>参数列表如下</p>
<p>AppID:<span style="color: red">{{$data['client_id']}}</span></p>
<p>redirect_uri:<span style="color: red">{{$data['redirect_uri']}}</span></p>
<p>state:<span style="color: red">{{$data['state']}}</span></p>
<p>跳转地址:<span style="color: red">{{$url}}</span></p>
</div>
<div class="form">
<form action="" method="post">
@csrf
<button type="submit">
<img src="images/qq_login.png">
</button>
</form>
</div>
</div>
</body>
</html>
code.blade.php
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
.main{
width: 800px;
height: 200px;
margin: 0 auto;
margin-top: 300px;
}
.params{
text-align: center;
}
.form{
margin-left: 280px;
}
</style>
<title>QQ三方登录</title>
</head>
<body>
<div class="main">
<div class="params">
<p>参数列表如下</p>
<p>AppID:<span style="color: red">{{$data['client_id']}}</span></p>
<p>appKey:<span style="color: red">{{$data['client_secret']}}</span></p>
<p>redirect_uri:<span style="color: red">{{$data['redirect_uri']}}</span></p>
<p>跳转地址:<span style="color: red">{{$url}}</span></p>
</div>
<div class="form">
<form action="" method="post">
@csrf
<button type="submit">用code换取令牌</button>
</form>
</div>
</div>
</body>
</html>
token.blade.php
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
.main{
width: 800px;
height: 200px;
margin: 0 auto;
margin-top: 300px;
}
.params{
text-align: center;
}
.form{
margin-left: 280px;
}
</style>
<title>QQ三方登录</title>
</head>
<body>
<div class="main">
<div class="params">
<p>参数列表如下</p>
<p>access_token:<span style="color: red">{{$data['access_token']}}</span></p>
<p>expires_in:<span style="color: red">{{$data['expires_in']}}</span></p>
<p>refresh_token:<span style="color: red">{{$data['refresh_token']}}</span></p>
</div>
<div class="form">
<input type="text" name="access_token" value = "{{$data['access_token']}}">
<button type="submit">用accessToken获取到用户open_id</button>
</div>
</div>
</body>
</html>
如果想用tp框架,修改以下视图渲染方式及删除csrf即可。
页面展示(index方法):
点击登录logo跳转到授权登录页:
授权并登录后接受code:
点击用code换取令牌后显示:
单独调用getOpenId方法,将令牌用get传递进去。就可以拿到用户信息了,此时仅仅需要诱导用户进行账号绑定即可,