第三方授权登录

理解OAuth

file

微信网页授权登录

微信公众平台

<?php
/**
 * Created by PhpStorm.
 * User: luish
 * Date: 2020/1/23
 * Time: 上午10:33
 */

/**
 * OAuth2.0微信授权登录实现
 */

// 回调地址
$url = urlencode("http://xx.xxx.xxx/wechat_oauth.php");
// 公众号的id和secret
$appid = 'xxxx';
$appsecret = 'xxxx';

$scope = 'snsapi_base';  //snsapi_userinfo 获取全部信息 ,snsapi_base 静默授权用户无感知,只能拿到openId

//// 获取code码,用于和微信服务器申请token。 注:依据OAuth2.0要求,此处授权登录需要用户端操作
if (!isset($_GET['code']) && !isset($_SESSION['code'])) {
    header('location:' . "https://open.weixin.qq.com/connect/oauth2/authorize?appid=$appid&redirect_uri=$url.'&response_type=code&scope=&state=1#wechat_redirect");
    exit;
}

// 依据code码去获取openid和access_token,自己的后台服务器直接向微信服务器申请即可
if (isset($_GET['code']) && !isset($_SESSION['token'])) {
    $_SESSION['code'] = $_GET['code'];

    $url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" . $appid .
        "&secret=" . $appsecret . "&code=" . $_GET['code'] . "&grant_type=authorization_code";
    $res = https_request($url);
    $res = (json_decode($res, true));
    $_SESSION['token'] = $res;

}

print_r($_SESSION);
print_r($res);

if ($scope == 'snsapi_base') {
// 依据申请到的access_token和openid,申请Userinfo信息。
    if (isset($_SESSION['token']['access_token'])) {
        $url = "https://api.weixin.qq.com/sns/userinfo?access_token=" . $_SESSION['token']['access_token'] . "&openid=" . $_SESSION['token']['openid'] . "&lang=zh_CN";
        echo $url;
        $res = https_request($url);
        $res = json_decode($res, true);

        $_SESSION['userinfo'] = $res;

    }

    print_r($_SESSION);

}

// cURL函数简单封装
function https_request($url, $data = null)
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
    if (!empty($data)) {
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    }
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($curl);
    curl_close($curl);
    return $output;
}

php 开发 oauth2 流程

Oauth2.0 是一个开源的授权协议,在全世界得到广泛应用,比较大的社交服务都支持了Oauth2.0 协议,例如 QQ,微博,微信。

Oauth2 协议的使用场景

假设有一个叫“教程集”的网站,可以通过读取用户在微信里的好友关系查询到还有谁也在学习教程,用户想使用该服务,就必须让“教程集”读取自己在微信里的好友关系。

微信只有得到用户的授权才会允许“教程集” 读取用户的好友关系,这时候传统的方式是,用户将微信帐号密码提交给“教程集”,“教程集”使用用户的帐号密码登录微信,再获取到用户的好友关系。

但是这样的做法有几个缺点:

  • 为了后续用户不需要再次输入微信帐号密码,“教程集”不得不保存用户的微信帐号密码;
  • 假设用户想收回对“教程集”的授权,只能修改密码,但是同时会使其他获得了用户授权的第三方程序全部失效;
  • 用户无法控制“教程集”的权限范围和有效时间,用户将微信帐号密码都提交给“教程集”,“教程集”获得了几乎所有权限。
  • 假设“教程集”被破解,所有用户的微信帐号密码同时泄露。

Oauth2 正是用来解决以上场景遇到的问题的。

Oauth2 协议的几个专业术语

  1. Third-party application: 第三方应用程序,又称“客户端”, 即上面例子中的“教程集”;
  2. HTTP service : HTTP服务提供商,即“服务提供商”, 即上面例子中的“微信”;
  3. Resource Owner: 资源所有者,即上面例子中的“用户”;
  4. User Agent:用户代理,在本文中即“浏览器”;
  5. Authorized server: 认证服务器,即服务提供商提供的专门用来做用户认证的服务器,在上面的例子中属于微信服务的一部分;
  6. Resource server: 资源服务器,即服务提供商提供的用户存放用户资源的服务器,在上面的例子中可以视为是一个查询用户好友关系的接口。

Oauth2 运行的一般流程

为了描述方便还是用微信举例子:

  1. 用打开客户端,客户端要求用户给予授权。
  2. 用户同意给予授权。
  3. 客户端使用上一步获取的授权码,向认证服务器换取令牌。
  4. 认证服务器检查客户端发来的授权码,确认无误,向客户端发放令牌。
  5. 客户端使用令牌,向资源服务器获取资源。
  6. 资源服务器验证令牌无误,同意向客户端开放资源。

其中最关键的是第 “2”,即客户端如何获取用户的授权,客户端拿到授权码就可以向认证服务器换取令牌,Oauth2 提供了4种授权的方式:

  • 授权码模式 (authorized code)
  • 简化模式 (implict)
  • 密码模式 (resource owner password credential)
  • 客户端模式( client credentials)

下面我们主要讲下常用的授权码模式。

授权码模式

授权码模式,即我们最常用的授权模式,目前微博、微信、QQ 等都是用的这种授权方式,这种授权模式是最严密,功能最完整的。

主要流程如下:

  1. 用户访问客户端,客户端将用户导向(一般是 302跳转)认证服务器,
  2. 认证服务器判断用户是否已登录,如果没有登录,则在认证服务器提供的登录界面进行登录
  3. 假设已经登录,用户选择是否给予客户端授权(认证服务器一般会提供授权界面,用户可以选择开放哪些资源给客户端),假设用户给予授权,认证服务器将导向客户端实现指定的“重定向URI”(rediret_uri),同时在 redirect_uri 上附一个授权码
  4. 客户端收到授权码,附上早先的“重定向URL”, 向认证服务器申请令牌。(这一步在后台完成,用户不可见)
  5. 认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access_token)和更新令牌(refresh_token)。
  6. 客户端使用 access_token 向资源服务器获取资源。

步骤1中涉及的参数:

参数名 必填 说明
response_type 此处必须为“code”
client_id 客户端id
redirect_uri 重定向 URI
scope 申请的授权范围
state 客户端当前状态,可以是任意值,认证服务器会原样返回这个参数

例子:

GET http://oauth2-server.dev/authorize.php?response_type=code&client_id=testclient&state=xyz&redirect_uri=http%3A%2F%2F127.0.0.1%3A8001%2F%2Fclient.php

步骤3 中,认证服务器回应客户端的URI,包含以下参数:

参数名 必填 说明
code 授权码, 该授权码有效期很短,例如30秒,且只能使用一次。
state 如果客户端中请求中包含这个参数,认证服务器的响应也必须包含同样的参数和值

例子

HTTP/1.1 302 FoundLocation: http://oauth2-client.dev/client.php?code=ef2d9cd1bc71d99fa4ad193beab1bff48ec65df4&state=xyz

步骤4 中,客户端向认证服务器申请令牌,包含以下参数:

参数名 必填 说明
grant_type 表示授权模式,此处值固定为 ”authorized_code”
code 上一步获得的授权码
redirect_uri 重定向URI,必须跟步骤1中的该参数值保持一致。
client_id 表示客户端id

例子:

POST /token.php HTTP/1.1Host: 127.0.0.1:8001Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&&redirect_uri=http%3A%2F%2F127.0.0.1%3A8001%2F%2Fclient.php

步骤5 中,认证服务器回复 HTTP 请求,包含以下参数:

参数名 必填 说明
access_token 访问令牌
token_type 令牌类型,可以是 bearer 或者 mac 类型
expire_in 表示过期时间,单位为秒,
refresh_token 用来获取下一次的令牌访问
scope 表示权限范围

例子:

HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-storePragma: no-cache {  "access_token":"2YotnFZFEjr1zCsicMWpAA",  "token_type":"example",  "expires_in":3600,  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"}

Cache-Control, 表示强制此接口不缓存。

PHP 实现Oauth2.0 授权码模式

#host
127.0.0.1 www.oauth2client.com
127.0.0.1 owww.oauth2server.com
composer require bshaffer/oauth2-server-php
composer require guzzlehttp/guzzle  
#sql
--
-- Database: `oath2_test`
--

-- --------------------------------------------------------

--
-- 表的结构 `oauth_access_tokens`
--

CREATE TABLE IF NOT EXISTS `oauth_access_tokens` (
  `access_token` varchar(40) NOT NULL,
  `client_id` varchar(80) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `scope` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_authorization_codes`
--

CREATE TABLE IF NOT EXISTS `oauth_authorization_codes` (
  `authorization_code` varchar(40) NOT NULL,
  `client_id` varchar(80) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `redirect_uri` varchar(2000) DEFAULT NULL,
  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `scope` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_clients`
--

CREATE TABLE IF NOT EXISTS `oauth_clients` (
  `client_id` varchar(80) NOT NULL,
  `client_secret` varchar(80) NOT NULL,
  `redirect_uri` varchar(2000) NOT NULL,
  `grant_types` varchar(80) DEFAULT NULL,
  `scope` varchar(100) DEFAULT NULL,
  `user_id` varchar(80) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- 转存表中的数据 `oauth_clients`
--

INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `grant_types`, `scope`, `user_id`) VALUES
('testclient', 'testpass', 'http://www.oauth2client.com/client.php', 'authorization_code', '', '');

-- --------------------------------------------------------

--
-- 表的结构 `oauth_jwt`
--

CREATE TABLE IF NOT EXISTS `oauth_jwt` (
  `client_id` varchar(80) NOT NULL,
  `subject` varchar(80) DEFAULT NULL,
  `public_key` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_refresh_tokens`
--

CREATE TABLE IF NOT EXISTS `oauth_refresh_tokens` (
  `refresh_token` varchar(40) NOT NULL,
  `client_id` varchar(80) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `scope` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_scopes`
--

CREATE TABLE IF NOT EXISTS `oauth_scopes` (
  `scope` text,
  `is_default` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_users`
--

CREATE TABLE IF NOT EXISTS `oauth_users` (
  `username` varchar(255) NOT NULL,
  `password` varchar(2000) DEFAULT NULL,
  `first_name` varchar(255) DEFAULT NULL,
  `last_name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

客服端

client.php

<?php
require_once  './vendor/autoload.php';
define('CLIENT_URL', 'http://oauth2client.com');  //第三方应用程序,又称“客户端”
define('SERVER_URL', 'http://oauth2server.com'); //服务提供商

define('REDIRECT_URI', CLIENT_URL.'/client.php');   //
define('RESOURCE_URL', SERVER_URL.'/resource.php');  //服务提供商

define('CLIENT_ID', 'testclient');
define('CLIENT_SECRET', 'testpass');

session_start();
function userInfo(){
    if(isset($_SESSION['username'])) {
        return $_SESSION;
    } else {
        return false;
    }
}

if(isset($_REQUEST['logout'])) {
    unset($_SESSION['username']);
    session_destroy();
}

$userInfo = userInfo();
/*
 * 接收用户中心返回的授权码
 */
if (isset($_REQUEST['code']) && $_SERVER['REQUEST_URI']) {
    //将认证服务器返回的授权码从 URL 中解析出来
    $code = substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], 'code=')+5, 40);

    // 步骤4 拿授权码去申请令牌
    $client = new GuzzleHttp\Client();
    $response = $client->request('POST', SERVER_URL.'/token.php', [
        'auth' => [CLIENT_ID, CLIENT_SECRET],
        'form_params'=> [
            'grant_type'=>'authorization_code',
            'code'=> $code,
            'redirect_uri'=> REDIRECT_URI,
        ]
    ]);

    $response = json_decode($response->getBody(), true);

    // 将令牌缓存到 SESSION中,方便后续访问
    $_SESSION['access_token'] = $response['access_token'];

    // 步骤6 使用令牌获取用户信息
    $response = $client->request('GET', RESOURCE_URL.'?access_token='.$_SESSION['access_token']);
    $response = json_decode($response->getBody(), true);
//
    $userInfo = $response['userInfo'];
    var_dump($response);die;
//    $_SESSION = array_merge($_SESSION, $userInfo);

}

// 步骤1,点击此链接跳转到认证中心
$auth_url = SERVER_URL."/authorize.php?response_type=code&client_id=testclient&state=xyz&redirect_uri=". (REDIRECT_URI);

?>

<?php if($userInfo): ?>
    欢迎 <?php echo $userInfo['username'];?>, 头像 <img src="<?php echo $userInfo['avatar']; ?>" alt="" />
    <a href="/index.php?logout=1">退出登录</a>
<?php else: ?>
    <a href="<?php echo $auth_url ?>">使用微信登录</a>
<?php endif;?>
</body>
</html>

服务端:

server.php(配置文件)

<?php
/** 配置 */
$dsn= 'mysql:dbname=oath2_test;host=127.0.0.1';
$username = 'root';
$password = 'root123';

// 错误报告(这毕竟是一个演示!)
ini_set('display_errors',1);error_reporting(E_ALL);

//user.endv.cn
// 自动加载
require_once('./vendor/autoload.php');
OAuth2\Autoloader::register();
$storage = new OAuth2\Storage\Pdo(array('dsn' => $dsn, 'username' => $username, 'password' => $password));

// 通过存储对象或对象数组存储的oauth2服务器类
$server = new OAuth2\Server($storage);

// 授权码 有效期只有30秒
$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage));

// 客户端证书
$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));

// 用户凭据
$server->addGrantType(new OAuth2\GrantType\UserCredentials($storage));
// 刷新令牌  启用这个会报错,原因未知
// $server->addGrantType(new OAuth2\GrantType\RefreshToken($refreshStorage))

authorize.php

<?php

/**
 *
 * 服务端验证页面
 * 1提示检查用户登录该服务提供商
 * 2返回authorized code
 */

define('CLIENT_URL', 'http://oauth2client.com');
define('SERVER_URL', 'http://oauth2server.com');
define('REDIRECT_URI', CLIENT_URL.'/index.php');
//define('RESOURCE_URL', SERVER_URL.'/resource.php');

define('CLIENT_ID', 'testclient');
define('CLIENT_SECRET', 'testpass');
require  './server.php';
$_SESSION['authorize_querystring'] = $_SERVER['QUERY_STRING'];

session_start();
$_SESSION['username'] = 'hl';  // login.php 为server端登录页面 暂模拟以登录
// 步骤2 判断如果没有登录则跳转到登录界面
//if(!isset($_SESSION['username']) && strpos($_SERVER['REQUEST_URI'], 'login.php') === false) {
//    header("Location: ".SERVER_URL.'/login.php');
//    exit;
//}

$request = OAuth2\Request::createFromGlobals();

$response = new \OAuth2\Response();

if(!$server->validateAuthorizeRequest($request, $response)) {

    $response->send();
    die;
}

if(empty($_POST)) {
    // 步骤3 ,用户已经在认证中心登录,用户选择是否开放授权给客户端
    exit('<form method="post">
  <label>是否授权给 '.$_GET['client_id'].'?</label><br />
  <input type="submit" name="authorized" value="yes">
  <input type="submit" name="authorized" value="no">
</form>  
    <a href="/login.php?logout=1">退出登录</a>
');
}

// print the authorization code if the user has authorized your client
$is_authorized = ($_POST['authorized'] === 'yes');
$server->handleAuthorizeRequest($request, $response, $is_authorized);

$response->send();

token.php

<?php
// include our OAuth2 Server object
require_once __DIR__.'/server.php';

//认证服务器检查客户端发来的授权码,确认无误,向客户端发放令牌
$server->handleTokenRequest(OAuth2\Request::createFromGlobals())->send();

resource.php

<?php
/**
 * Created by PhpStorm.
 * User: luish
 * Date: 2020/1/23
 * Time: 下午5:58
 */

// include our OAuth2 Server object
require_once __DIR__ . '/server.php';

// Handle a request for an OAuth2.0 Access Token and send the response to the client
if (!$server->verifyResourceRequest(OAuth2\Request::createFromGlobals())) {
    $server->getResponse()->send();
    die;
}
echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'));

可以转载,但必须说明出处

发表评论