oauth2.0服务端与客户端搭建

oauth2.0服务端与客户端搭建 - 推酷

今天搭建了oauth2.0服务端与客户端。把搭建的过程记录一下。具体实现的功能是:client.ruanwenwu.cn的用户能够通过 server.ruanwenwu.cn的用户名和密码登陆client.ruanwenwu.cn。并且登陆 后,client.ruanwenwu.cn的用户能获取server.ruanwenwu.cn哪里的一些资源。

我的个人博客原文地址: http://www.ruanwenwu.cn/2015/12/oauth-server-and-client-install.html 

一、oauth2.0的作用

1、搭建第三方登录平台(就像你用很多网站支持qq登录。其实是qq提供的oauth2.0服务端支持。网站作为第三方,只要用oauth2.0标准请求服务端,求能得到用户信息,并实现登录)。

2、公共资源管理。

就 像你(Y)想通过一个打印照片的网站(A)来打印你存放在google(B)的照片。你就要通过这个A来获取B的照片。B肯定要验证是否是他的用户Y的请 求,验证通过才把照片传递给A实现打印。问题是Y不想把账号密码直接给A。用oauth2.0就是来解决这个问题的:

A先 把Y导向B的站点进行授权。授权通过,B向A发送oauth_code。A拿到这个oauth_code,连同A的client_id和 oauth_secrete发送给B,如果数据正确,B就会给A发送oauth_token。有了这个token,A就可以获取B的相关资源的。

基本的流程是这样。下面是具体的实现过程。

二、oauth2.0服务端的搭建。

oauth2.0的服务端分为验证服务器和资源服务器。这两个服务器也可以是同一个服务器(只不过把这两个功能用一个 服务器来完成罢了)。

说明:我的实现都在Thinkphp3.2.3框架下实现。

1、下载thinkphp3.2.3。

2、配置虚拟机server.ruanwenwu.cn

3、在Thinkphp的Home应用下修改配置文件(server.ruanwenwu.cn/Application/Home/Conf/config.php)如下所示:

<?php
return array(
    //'配置项'=>'配置值'
    //数据库设置
        'DB_TYPE' => 'mysql',
        'DB_HOST' => '127.0.0.1',//localhost
        'DB_NAME' => 'oauth',
        'DB_USER' => 'root',
        'DB_PWD'  => 'root',
        'DB_PORT' => '3306',
    

    'SCOPE_INFO'  => array(    //这是授权选项。根据你自己的项目来
        array(
            'name' => '个人信息',
            'value' => 'basicinfo'
        ),
        array(
            'name' => '论坛发帖回帖',
            'value' => 'bbsinfo'
        ),
    ),
    
    'OAUTH2_CODES_TABLE'   =>'oauth_code',    //这里是oauth项目需要用的三个基础表
    'OAUTH2_CLIENTS_TABLE' =>'oauth_client',
    'OAUTH2_TOKEN_TABLE'   =>'oauth_token',
    
    'SECRETKYE' => 'Mumayi!@#',    //下面是一些网站自定义的项目。可以根据自己的情况来写或者不写
    //session 有效期
    'SESSION_EXPIRES' => 1200,
    //key 有效期
    'PASS_KEY_EXPIRES' => 86400,
    //key 有效期
    'PHONE_KEY_EXPIRES' => 300,
    //key 加密 整型 数字 必须为 int
    'PASS_KEY_CALC' => 1314,
);

4、建oauth表。

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for oauth_client
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client`;
CREATE TABLE `oauth_client` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `client_id` varchar(32) NOT NULL,
  `client_secret` varchar(32) NOT NULL,
  `redirect_uri` varchar(200) NOT NULL,
  `create_time` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `client_id` varchar(32) NOT NULL,
  `user_id` int(11) NOT NULL DEFAULT '1',
  `code` varchar(40) NOT NULL,
  `redirect_uri` varchar(200) NOT NULL,
  `expires` int(11) NOT NULL,
  `scope` varchar(250) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=57 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for oauth_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_token`;
CREATE TABLE `oauth_token` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `client_id` varchar(32) NOT NULL,
  `user_id` int(11) NOT NULL,
  `access_token` varchar(40) NOT NULL,
  `refresh_token` varchar(40) NOT NULL,
  `expires_in` int(11) NOT NULL,
  `scope` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;

5、引入oauth2.0服务端类文件。

5-1、在\server.ruanwenwu.cn\ThinkPHP\Library\Vendor\目录下建立oauth目录。

5-2、引入oauth2.0服务端PHP脚本。

在刚建立的oauth目录下引入OAuth2.class.php这个文件基本不用做修改。我们的操作都在继承这个类的子类上完成。类的代码如下:

<?php

/**
 * @mainpage
 * OAuth 2.0 server in PHP, originally written for
 * <a href="http://www.opendining.net/"> Open Dining</a>. Supports
 * <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-10">IETF draft v10</a>.
 *
 * Source repo has sample servers implementations for
 * <a href="http://php.net/manual/en/book.pdo.php"> PHP Data Objects</a> and
 * <a href="http://www.mongodb.org/">MongoDB</a>. Easily adaptable to other
 * storage engines.
 *
 * PHP Data Objects supports a variety of databases, including MySQL,
 * Microsoft SQL Server, SQLite, and Oracle, so you can try out the sample
 * to see how it all works.
 *
 * We're expanding the wiki to include more helpful documentation, but for
 * now, your best bet is to view the oauth.php source - it has lots of
 * comments.
 *
 * @author Tim Ridgely <tim.ridgely@gmail.com>
 * @author Aaron Parecki <aaron@parecki.com>
 * @author Edison Wong <hswong3i@pantarei-design.com>
 *
 * @see http://code.google.com/p/oauth2-php/
 */


/**
 * The default duration in seconds of the access token lifetime.
 */
define("OAUTH2_DEFAULT_ACCESS_TOKEN_LIFETIME", 3600);

/**
 * The default duration in seconds of the authorization code lifetime.
 */
define("OAUTH2_DEFAULT_AUTH_CODE_LIFETIME", 30);

/**
 * The default duration in seconds of the refresh token lifetime.
 */
define("OAUTH2_DEFAULT_REFRESH_TOKEN_LIFETIME", 1209600);


/**
 * @defgroup oauth2_section_2 Client Credentials
 * @{
 *
 * When interacting with the authorization server, the client identifies
 * itself using a client identifier and authenticates using a set of
 * client credentials. This specification provides one mechanism for
 * authenticating the client using password credentials.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-2
 */

/**
 * Regex to filter out the client identifier (described in Section 2 of IETF draft).
 *
 * IETF draft does not prescribe a format for these, however I've arbitrarily
 * chosen alphanumeric strings with hyphens and underscores, 3-32 characters
 * long.
 *
 * Feel free to change.
 */
define("OAUTH2_CLIENT_ID_REGEXP", "/^[a-z0-9-_]{3,32}$/i");

/**
 * @}
 */


/**
 * @defgroup oauth2_section_3 Obtaining End-User Authorization
 * @{
 *
 * When the client interacts with an end-user, the end-user MUST first
 * grant the client authorization to access its protected resources.
 * Once obtained, the end-user access grant is expressed as an
 * authorization code which the client uses to obtain an access token.
 * To obtain an end-user authorization, the client sends the end-user to
 * the end-user authorization endpoint.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3
 */

/**
 * Denotes "token" authorization response type.
 */
define("OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN", "token");

/**
 * Denotes "code" authorization response type.
 */
define("OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE", "code");

/**
 * Denotes "code-and-token" authorization response type.
 */
define("OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN", "code-and-token");

/**
 * Regex to filter out the authorization response type.
 */
define("OAUTH2_AUTH_RESPONSE_TYPE_REGEXP", "/^(token|code|code-and-token)$/");

/**
 * @}
 */


/**
 * @defgroup oauth2_section_4 Obtaining an Access Token
 * @{
 *
 * The client obtains an access token by authenticating with the
 * authorization server and presenting its access grant (in the form of
 * an authorization code, resource owner credentials, an assertion, or a
 * refresh token).
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4
 */

/**
 * Denotes "authorization_code" grant types (for token obtaining).
 */
define("OAUTH2_GRANT_TYPE_AUTH_CODE", "authorization_code");

/**
 * Denotes "password" grant types (for token obtaining).
 */
define("OAUTH2_GRANT_TYPE_USER_CREDENTIALS", "password");

/**
 * Denotes "assertion" grant types (for token obtaining).
 */
define("OAUTH2_GRANT_TYPE_ASSERTION", "assertion");

/**
 * Denotes "refresh_token" grant types (for token obtaining).
 */
define("OAUTH2_GRANT_TYPE_REFRESH_TOKEN", "refresh_token");

/**
 * Denotes "none" grant types (for token obtaining).
 */
define("OAUTH2_GRANT_TYPE_NONE", "none");

/**
 * Regex to filter out the grant type.
 */
define("OAUTH2_GRANT_TYPE_REGEXP", "/^(authorization_code|password|assertion|refresh_token|none)$/");

/**
 * @}
 */


/**
 * @defgroup oauth2_section_5 Accessing a Protected Resource
 * @{
 *
 * Clients access protected resources by presenting an access token to
 * the resource server. Access tokens act as bearer tokens, where the
 * token string acts as a shared symmetric secret. This requires
 * treating the access token with the same care as other secrets (e.g.
 * end-user passwords). Access tokens SHOULD NOT be sent in the clear
 * over an insecure channel.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5
 */

/**
 * Used to define the name of the OAuth access token parameter (POST/GET/etc.).
 *
 * IETF Draft sections 5.1.2 and 5.1.3 specify that it should be called
 * "oauth_token" but other implementations use things like "access_token".
 *
 * I won't be heartbroken if you change it, but it might be better to adhere
 * to the spec.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.2
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.3
 */
define("OAUTH2_TOKEN_PARAM_NAME", "oauth_token");

/**
 * @}
 */


/**
 * @defgroup oauth2_http_status HTTP status code
 * @{
 */

/**
 * "Found" HTTP status code.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3
 */
define("OAUTH2_HTTP_FOUND", "302 Found");

/**
 * "Bad Request" HTTP status code.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_HTTP_BAD_REQUEST", "400 Bad Request");

/**
 * "Unauthorized" HTTP status code.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_HTTP_UNAUTHORIZED", "401 Unauthorized");

/**
 * "Forbidden" HTTP status code.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_HTTP_FORBIDDEN", "403 Forbidden");

/**
 * @}
 */


/**
 * @defgroup oauth2_error Error handling
 * @{
 *
 * @todo Extend for i18n.
 */

/**
 * The request is missing a required parameter, includes an unsupported
 * parameter or parameter value, or is otherwise malformed.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_ERROR_INVALID_REQUEST", "invalid_request");

/**
 * The client identifier provided is invalid.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1
 */
define("OAUTH2_ERROR_INVALID_CLIENT", "invalid_client");

/**
 * The client is not authorized to use the requested response type.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1
 */
define("OAUTH2_ERROR_UNAUTHORIZED_CLIENT", "unauthorized_client");

/**
 * The redirection URI provided does not match a pre-registered value.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 */
define("OAUTH2_ERROR_REDIRECT_URI_MISMATCH", "redirect_uri_mismatch");

/**
 * The end-user or authorization server denied the request.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 */
define("OAUTH2_ERROR_USER_DENIED", "access_denied");

/**
 * The requested response type is not supported by the authorization server.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 */
define("OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE", "unsupported_response_type");

/**
 * The requested scope is invalid, unknown, or malformed.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1
 */
define("OAUTH2_ERROR_INVALID_SCOPE", "invalid_scope");

/**
 * The provided access grant is invalid, expired, or revoked (e.g. invalid
 * assertion, expired authorization token, bad end-user password credentials,
 * or mismatching authorization code and redirection URI).
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1
 */
define("OAUTH2_ERROR_INVALID_GRANT", "invalid_grant");

/**
 * The access grant included - its type or another attribute - is not
 * supported by the authorization server.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1
 */
define("OAUTH2_ERROR_UNSUPPORTED_GRANT_TYPE", "unsupported_grant_type");

/**
 * The access token provided is invalid. Resource servers SHOULD use this
 * error code when receiving an expired token which cannot be refreshed to
 * indicate to the client that a new authorization is necessary. The resource
 * server MUST respond with the HTTP 401 (Unauthorized) status code.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_ERROR_INVALID_TOKEN", "invalid_token");

/**
 * The access token provided has expired. Resource servers SHOULD only use
 * this error code when the client is expected to be able to handle the
 * response and request a new access token using the refresh token issued
 * with the expired access token. The resource server MUST respond with the
 * HTTP 401 (Unauthorized) status code.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_ERROR_EXPIRED_TOKEN", "expired_token");

/**
 * The request requires higher privileges than provided by the access token.
 * The resource server SHOULD respond with the HTTP 403 (Forbidden) status
 * code and MAY include the "scope" attribute with the scope necessary to
 * access the protected resource.
 *
 * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1
 */
define("OAUTH2_ERROR_INSUFFICIENT_SCOPE", "insufficient_scope");

/**
 * @}
 */

/**
 * OAuth2.0 draft v10 server-side implementation.
 *
 * @author Originally written by Tim Ridgely <tim.ridgely@gmail.com>.
 * @author Updated to draft v10 by Aaron Parecki <aaron@parecki.com>.
 * @author Debug, coding style clean up and documented by Edison Wong <hswong3i@pantarei-design.com>.
 */
abstract class OAuth2 {

  /**
   * Array of persistent variables stored.
   */
  protected $conf = array();

  /**
   * Returns a persistent variable.
   *
   * To avoid problems, always use lower case for persistent variable names.
   *
   * @param $name
   *   The name of the variable to return.
   * @param $default
   *   The default value to use if this variable has never been set.
   *
   * @return
   *   The value of the variable.
   */
  public function getVariable($name, $default = NULL) {
    return isset($this->conf[$name]) ? $this->conf[$name] : $default;
  }

  /**
   * Sets a persistent variable.
   *
   * To avoid problems, always use lower case for persistent variable names.
   *
   * @param $name
   *   The name of the variable to set.
   * @param $value
   *   The value to set.
   */
  public function setVariable($name, $value) {
    $this->conf[$name] = $value;
    return $this;
  }

  // Subclasses must implement the following functions.

  /**
   * Make sure that the client credentials is valid.
   *
   * @param $client_id
   *   Client identifier to be check with.
   * @param $client_secret
   *   (optional) If a secret is required, check that they've given the right one.
   *
   * @return
   *   TRUE if client credentials are valid, and MUST return FALSE if invalid.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-2.1
   *
   * @ingroup oauth2_section_2
   */
  abstract protected function checkClientCredentials($client_id, $client_secret = NULL);

  /**
   * Get the registered redirect URI of corresponding client_id.
   *
   * OAuth says we should store request URIs for each registered client.
   * Implement this function to grab the stored URI for a given client id.
   *
   * @param $client_id
   *   Client identifier to be check with.
   *
   * @return
   *   Registered redirect URI of corresponding client identifier, and MUST
   *   return FALSE if the given client does not exist or is invalid.
   *
   * @ingroup oauth2_section_3
   */
  abstract protected function getRedirectUri($client_id);

  /**
   * Look up the supplied oauth_token from storage.
   *
   * We need to retrieve access token data as we create and verify tokens.
   *
   * @param $oauth_token
   *   oauth_token to be check with.
   *
   * @return
   *   An associative array as below, and return NULL if the supplied oauth_token
   *   is invalid:
   *   - client_id: Stored client identifier.
   *   - expires: Stored expiration in unix timestamp.
   *   - scope: (optional) Stored scope values in space-separated string.
   *
   * @ingroup oauth2_section_5
   */
  abstract protected function getAccessToken($oauth_token);

  /**
   * Store the supplied access token values to storage.
   *
   * We need to store access token data as we create and verify tokens.
   *
   * @param $oauth_token
   *   oauth_token to be stored.
   * @param $client_id
   *   Client identifier to be stored.
   * @param $expires
   *   Expiration to be stored.
   * @param $user_id
   *   User ID
   * @param $scope
   *   (optional) Scopes to be stored in space-separated string.
   *
   * @ingroup oauth2_section_4
   */
  abstract protected function setAccessToken($oauth_token, $client_id, $expires, $user_id, $scope=NULL, $refresh_token='');

  // Stuff that should get overridden by subclasses.
  //
  // I don't want to make these abstract, because then subclasses would have
  // to implement all of them, which is too much work.
  //
  // So they're just stubs. Override the ones you need.

  /**
   * Return supported grant types.
   *
   * You should override this function with something, or else your OAuth
   * provider won't support any grant types!
   *
   * @return
   *   A list as below. If you support all grant types, then you'd do:
   * @code
   * return array(
   *   OAUTH2_GRANT_TYPE_AUTH_CODE,
   *   OAUTH2_GRANT_TYPE_USER_CREDENTIALS,
   *   OAUTH2_GRANT_TYPE_ASSERTION,
   *   OAUTH2_GRANT_TYPE_REFRESH_TOKEN,
   *   OAUTH2_GRANT_TYPE_NONE,
   * );
   * @endcode
   *
   * @ingroup oauth2_section_4
   */
  protected function getSupportedGrantTypes() {
    return array();
  }

  /**
   * Return supported authorization response types.
   *
   * You should override this function with your supported response types.
   *
   * @return
   *   A list as below. If you support all authorization response types,
   *   then you'd do:
   * @code
   * return array(
   *   OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE,
   *   OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN,
   *   OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN,
   * );
   * @endcode
   *
   * @ingroup oauth2_section_3
   */
  protected function getSupportedAuthResponseTypes() {
    return array(
      OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE,
      OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN,
      OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN
    );
  }

  /**
   * Return supported scopes.
   *
   * If you want to support scope use, then have this function return a list
   * of all acceptable scopes (used to throw the invalid-scope error).
   *
   * @return
   *   A list as below, for example:
   * @code
   * return array(
   *   'my-friends',
   *   'photos',
   *   'whatever-else',
   * );
   * @endcode
   *
   * @ingroup oauth2_section_3
   */
  protected function getSupportedScopes() {
    return array();
  }

  /**
   * Check restricted authorization response types of corresponding Client
   * identifier.
   *
   * If you want to restrict clients to certain authorization response types,
   * override this function.
   *
   * @param $client_id
   *   Client identifier to be check with.
   * @param $response_type
   *   Authorization response type to be check with, would be one of the
   *   values contained in OAUTH2_AUTH_RESPONSE_TYPE_REGEXP.
   *
   * @return
   *   TRUE if the authorization response type is supported by this
   *   client identifier, and FALSE if it isn't.
   *
   * @ingroup oauth2_section_3
   */
  protected function checkRestrictedAuthResponseType($client_id, $response_type) {
    return TRUE;
  }

  /**
   * Check restricted grant types of corresponding client identifier.
   *
   * If you want to restrict clients to certain grant types, override this
   * function.
   *
   * @param $client_id
   *   Client identifier to be check with.
   * @param $grant_type
   *   Grant type to be check with, would be one of the values contained in
   *   OAUTH2_GRANT_TYPE_REGEXP.
   *
   * @return
   *   TRUE if the grant type is supported by this client identifier, and
   *   FALSE if it isn't.
   *
   * @ingroup oauth2_section_4
   */
  protected function checkRestrictedGrantType($client_id, $grant_type) {
    return TRUE;
  }

  // Functions that help grant access tokens for various grant types.

  /**
   * Fetch authorization code data (probably the most common grant type).
   *
   * Retrieve the stored data for the given authorization code.
   *
   * Required for OAUTH2_GRANT_TYPE_AUTH_CODE.
   *
   * @param $code
   *   Authorization code to be check with.
   *
   * @return
   *   An associative array as below, and NULL if the code is invalid:
   *   - client_id: Stored client identifier.
   *   - redirect_uri: Stored redirect URI.
   *   - expires: Stored expiration in unix timestamp.
   *   - scope: (optional) Stored scope values in space-separated string.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.1
   *
   * @ingroup oauth2_section_4
   */
  protected function getAuthCode($code) {
    return NULL;
  }

  /**
   * Take the provided authorization code values and store them somewhere.
   *
   * This function should be the storage counterpart to getAuthCode().
   *
   * If storage fails for some reason, we're not currently checking for
   * any sort of success/failure, so you should bail out of the script
   * and provide a descriptive fail message.
   *
   * Required for OAUTH2_GRANT_TYPE_AUTH_CODE.
   *
   * @param $code
   *   Authorization code to be stored.
   * @param $client_id
   *   Client identifier to be stored.
   * @param $redirect_uri
   *   Redirect URI to be stored.
   * @param $expires
   *   Expiration to be stored.
   * @param $scope
   *   (optional) Scopes to be stored in space-separated string.
   *
   * @ingroup oauth2_section_4
   */
  protected function setAuthCode($code, $client_id, $redirect_uri, $expires, $userId, $scope = NULL) {
  }

  /**
   * Grant access tokens for basic user credentials.
   *
   * Check the supplied username and password for validity.
   *
   * You can also use the $client_id param to do any checks required based
   * on a client, if you need that.
   *
   * Required for OAUTH2_GRANT_TYPE_USER_CREDENTIALS.
   *
   * @param $client_id
   *   Client identifier to be check with.
   * @param $username
   *   Username to be check with.
   * @param $password
   *   Password to be check with.
   *
   * @return
   *   TRUE if the username and password are valid, and FALSE if it isn't.
   *   Moreover, if the username and password are valid, and you want to
   *   verify the scope of a user's access, return an associative array
   *   with the scope values as below. We'll check the scope you provide
   *   against the requested scope before providing an access token:
   * @code
   * return array(
   *   'scope' => <stored scope values (space-separated string)>,
   * );
   * @endcode
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.2
   *
   * @ingroup oauth2_section_4
   */
  protected function checkUserCredentials($client_id, $username, $password) {
    return FALSE;
  }

  /**
   * Grant access tokens for assertions.
   *
   * Check the supplied assertion for validity.
   *
   * You can also use the $client_id param to do any checks required based
   * on a client, if you need that.
   *
   * Required for OAUTH2_GRANT_TYPE_ASSERTION.
   *
   * @param $client_id
   *   Client identifier to be check with.
   * @param $assertion_type
   *   The format of the assertion as defined by the authorization server.
   * @param $assertion
   *   The assertion.
   *
   * @return
   *   TRUE if the assertion is valid, and FALSE if it isn't. Moreover, if
   *   the assertion is valid, and you want to verify the scope of an access
   *   request, return an associative array with the scope values as below.
   *   We'll check the scope you provide against the requested scope before
   *   providing an access token:
   * @code
   * return array(
   *   'scope' => <stored scope values (space-separated string)>,
   * );
   * @endcode
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3
   *
   * @ingroup oauth2_section_4
   */
  protected function checkAssertion($client_id, $assertion_type, $assertion) {
    return FALSE;
  }

  /**
   * Grant refresh access tokens.
   *
   * Retrieve the stored data for the given refresh token.
   *
   * Required for OAUTH2_GRANT_TYPE_REFRESH_TOKEN.
   *
   * @param $refresh_token
   *   Refresh token to be check with.
   *
   * @return
   *   An associative array as below, and NULL if the refresh_token is
   *   invalid:
   *   - client_id: Stored client identifier.
   *   - expires: Stored expiration unix timestamp.
   *   - scope: (optional) Stored scope values in space-separated string.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.4
   *
   * @ingroup oauth2_section_4
   */
  protected function getRefreshToken($refresh_token) {
    return NULL;
  }

  /**
   * Take the provided refresh token values and store them somewhere.
   *
   * This function should be the storage counterpart to getRefreshToken().
   *
   * If storage fails for some reason, we're not currently checking for
   * any sort of success/failure, so you should bail out of the script
   * and provide a descriptive fail message.
   *
   * Required for OAUTH2_GRANT_TYPE_REFRESH_TOKEN.
   *
   * @param $refresh_token
   *   Refresh token to be stored.
   * @param $client_id
   *   Client identifier to be stored.
   * @param $expires
   *   expires to be stored.
   * @param $scope
   *   (optional) Scopes to be stored in space-separated string.
   *
   * @ingroup oauth2_section_4
   */
  protected function setRefreshToken($refresh_token, $client_id, $expires, $scope = NULL) {
    return;
  }

  /**
   * Expire a used refresh token.
   *
   * This is not explicitly required in the spec, but is almost implied.
   * After granting a new refresh token, the old one is no longer useful and
   * so should be forcibly expired in the data store so it can't be used again.
   *
   * If storage fails for some reason, we're not currently checking for
   * any sort of success/failure, so you should bail out of the script
   * and provide a descriptive fail message.
   *
   * @param $refresh_token
   *   Refresh token to be expirse.
   *
   * @ingroup oauth2_section_4
   */
  protected function unsetRefreshToken($refresh_token) {
    return;
  }

  /**
   * Grant access tokens for the "none" grant type.
   *
   * Not really described in the IETF Draft, so I just left a method
   * stub... Do whatever you want!
   *
   * Required for OAUTH2_GRANT_TYPE_NONE.
   *
   * @ingroup oauth2_section_4
   */
  protected function checkNoneAccess($client_id) {
    return FALSE;
  }

  /**
   * Get default authentication realm for WWW-Authenticate header.
   *
   * Change this to whatever authentication realm you want to send in a
   * WWW-Authenticate header.
   *
   * @return
   *   A string that you want to send in a WWW-Authenticate header.
   *
   * @ingroup oauth2_error
   */
  protected function getDefaultAuthenticationRealm() {
    return "Service";
  }

  // End stuff that should get overridden.

  /**
   * Creates an OAuth2.0 server-side instance.
   *
   * @param $config
   *   An associative array as below:
   *   - access_token_lifetime: (optional) The lifetime of access token in
   *     seconds.
   *   - auth_code_lifetime: (optional) The lifetime of authorization code in
   *     seconds.
   *   - refresh_token_lifetime: (optional) The lifetime of refresh token in
   *     seconds.
   *   - display_error: (optional) Whether to show verbose error messages in
   *     the response.
   */
  public function __construct($config = array()) {
    foreach ($config as $name => $value) {
      $this->setVariable($name, $value);
    }
  }

  // Resource protecting (Section 5).

  /**
   * Check that a valid access token has been provided.
   *
   * The scope parameter defines any required scope that the token must have.
   * If a scope param is provided and the token does not have the required
   * scope, we bounce the request.
   *
   * Some implementations may choose to return a subset of the protected
   * resource (i.e. "public" data) if the user has not provided an access
   * token or if the access token is invalid or expired.
   *
   * The IETF spec says that we should send a 401 Unauthorized header and
   * bail immediately so that's what the defaults are set to.
   *
   * @param $scope
   *   A space-separated string of required scope(s), if you want to check
   *   for scope.
   * @param $exit_not_present
   *   If TRUE and no access token is provided, send a 401 header and exit,
   *   otherwise return FALSE.
   * @param $exit_invalid
   *   If TRUE and the implementation of getAccessToken() returns NULL, exit,
   *   otherwise return FALSE.
   * @param $exit_expired
   *   If TRUE and the access token has expired, exit, otherwise return FALSE.
   * @param $exit_scope
   *   If TRUE the access token does not have the required scope(s), exit,
   *   otherwise return FALSE.
   * @param $realm
   *   If you want to specify a particular realm for the WWW-Authenticate
   *   header, supply it here.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5
   *
   * @ingroup oauth2_section_5
   */
  public function verifyAccessToken($scope = NULL, $exit_not_present = TRUE, $exit_invalid = TRUE, $exit_expired = TRUE, $exit_scope = TRUE, $realm = NULL) {
    $token_param = $this->getAccessTokenParams();
    if ($token_param === FALSE) // Access token was not provided
      return $exit_not_present ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_BAD_REQUEST, $realm, OAUTH2_ERROR_INVALID_REQUEST, 'The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.', NULL, $scope) : FALSE;
    // Get the stored token data (from the implementing subclass)
    $token = $this->getAccessToken($token_param);
    if ($token === NULL)
      return $exit_invalid ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_UNAUTHORIZED, $realm, OAUTH2_ERROR_INVALID_TOKEN, 'The access token provided is invalid.', NULL, $scope) : FALSE;

    // Check token expiration (I'm leaving this check separated, later we'll fill in better error messages)
    if (isset($token["expires"]) && time() > $token["expires"])
      return $exit_expired ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_UNAUTHORIZED, $realm, OAUTH2_ERROR_EXPIRED_TOKEN, 'The access token provided has expired.', NULL, $scope) : FALSE;

    // Check scope, if provided
    // If token doesn't have a scope, it's NULL/empty, or it's insufficient, then throw an error
    if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->checkScope($scope, $token["scope"])))
      return $exit_scope ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_FORBIDDEN, $realm, OAUTH2_ERROR_INSUFFICIENT_SCOPE, 'The request requires higher privileges than provided by the access token.', NULL, $scope) : FALSE;

    return TRUE;
  }

  /**
   * Check if everything in required scope is contained in available scope.
   *
   * @param $required_scope
   *   Required scope to be check with.
   * @param $available_scope
   *   Available scope to be compare with.
   *
   * @return
   *   TRUE if everything in required scope is contained in available scope,
   *   and False if it isn't.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5
   *
   * @ingroup oauth2_section_5
   */
  private function checkScope($required_scope, $available_scope) {
    // The required scope should match or be a subset of the available scope
    if (!is_array($required_scope))
      $required_scope = explode(" ", $required_scope);

    if (!is_array($available_scope))
      $available_scope = explode(" ", $available_scope);

    return (count(array_diff($required_scope, $available_scope)) == 0);
  }

  /**
   * Pulls the access token out of the HTTP request.
   *
   * Either from the Authorization header or GET/POST/etc.
   *
   * @return
   *   Access token value if present, and FALSE if it isn't.
   *
   * @todo Support PUT or DELETE.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1
   *
   * @ingroup oauth2_section_5
   */
  private function getAccessTokenParams() {
    $auth_header = $this->getAuthorizationHeader();
    if ($auth_header !== FALSE) {
      // Make sure only the auth header is set
      if (isset($_GET[OAUTH2_TOKEN_PARAM_NAME]) || isset($_POST[OAUTH2_TOKEN_PARAM_NAME]))
        $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Auth token found in GET or POST when token present in header');

      $auth_header = trim($auth_header);

      // Make sure it's Token authorization
      if (strcmp(substr($auth_header, 0, 5), "OAuth ") !== 0)
        $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Auth header found that doesn\'t start with "OAuth"');

      // Parse the rest of the header
      if (preg_match('/\s*OAuth\s*="(.+)"/', substr($auth_header, 5), $matches) == 0 || count($matches) < 2)
        $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Malformed auth header');

      return $matches[1];
    }

    if (isset($_GET[OAUTH2_TOKEN_PARAM_NAME])) {
      if (isset($_POST[OAUTH2_TOKEN_PARAM_NAME])) // Both GET and POST are not allowed
        $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Only send the token in GET or POST, not both');

      return $_GET[OAUTH2_TOKEN_PARAM_NAME];
    }

    if (isset($_POST[OAUTH2_TOKEN_PARAM_NAME]))
      return $_POST[OAUTH2_TOKEN_PARAM_NAME];

    return FALSE;
  }

  // Access token granting (Section 4).

  /**
   * Grant or deny a requested access token.
   *
   * This would be called from the "/token" endpoint as defined in the spec.
   * Obviously, you can call your endpoint whatever you want.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4
   *
   * @ingroup oauth2_section_4
   */
  public function grantAccessToken() {

    $filters = array(
      "grant_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => OAUTH2_GRANT_TYPE_REGEXP), "flags" => FILTER_REQUIRE_SCALAR),
      "scope" => array("flags" => FILTER_REQUIRE_SCALAR),
      "code" => array("flags" => FILTER_REQUIRE_SCALAR),
      "redirect_uri" => array("filter" => FILTER_SANITIZE_URL),
      "username" => array("flags" => FILTER_REQUIRE_SCALAR),
      "password" => array("flags" => FILTER_REQUIRE_SCALAR),
      "assertion_type" => array("flags" => FILTER_REQUIRE_SCALAR),
      "assertion" => array("flags" => FILTER_REQUIRE_SCALAR),
      "refresh_token" => array("flags" => FILTER_REQUIRE_SCALAR),
    );
    
    $input = filter_input_array(INPUT_POST, $filters);
    // Grant Type must be specified.
    if (!$input["grant_type"])
      $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Invalid grant_type parameter or parameter missing');
    
    // Make sure we've implemented the requested grant type
    if (!in_array($input["grant_type"], $this->getSupportedGrantTypes()))
      $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_UNSUPPORTED_GRANT_TYPE);

    // Authorize the client
    $client = $this->getClientCredentials();

    if ($this->checkClientCredentials($client[0], $client[1]) === FALSE)
      $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_CLIENT);

    if (!$this->checkRestrictedGrantType($client[0], $input["grant_type"]))
      $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_UNAUTHORIZED_CLIENT);

    // Do the granting
    switch ($input["grant_type"]) {
      case OAUTH2_GRANT_TYPE_AUTH_CODE:
        if (!$input["code"] || !$input["redirect_uri"])
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST);

        $stored = $this->getAuthCode($input["code"]);
        // Ensure that the input uri starts with the stored uri
        if ($stored === NULL || (strcasecmp(substr($input["redirect_uri"], 0, strlen($stored["redirect_uri"])), $stored["redirect_uri"]) !== 0) || $client[0] != $stored["client_id"])
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT);

        if ($stored["expires"] < time())
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_EXPIRED_TOKEN);

        break;
      case OAUTH2_GRANT_TYPE_USER_CREDENTIALS:
        if (!$input["username"] || !$input["password"])
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Missing parameters. "username" and "password" required');

        $stored = $this->checkUserCredentials($client[0], $input["username"], $input["password"]);

        if ($stored === FALSE)
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT);

        break;
      case OAUTH2_GRANT_TYPE_ASSERTION:
        if (!$input["assertion_type"] || !$input["assertion"])
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST);

        $stored = $this->checkAssertion($client[0], $input["assertion_type"], $input["assertion"]);

        if ($stored === FALSE)
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT);

        break;
      case OAUTH2_GRANT_TYPE_REFRESH_TOKEN:
        if (!$input["refresh_token"])
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'No "refresh_token" parameter found');

        $stored = $this->getRefreshToken($input["refresh_token"]);

        if ($stored === NULL || $client[0] != $stored["client_id"])
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT);

        if ($stored["expires"] < time())
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_EXPIRED_TOKEN);

        // store the refresh token locally so we can delete it when a new refresh token is generated
        $this->setVariable('_old_refresh_token', $stored["token"]);

        break;
      case OAUTH2_GRANT_TYPE_NONE:
        $stored = $this->checkNoneAccess($client[0]);

        if ($stored === FALSE)
          $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST);
    }

    // Check scope, if provided
    if ($input["scope"] && (!is_array($stored) || !isset($stored["scope"]) || !$this->checkScope($input["scope"], $stored["scope"])))
      $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_SCOPE);

    if (!$input["scope"])
      $input["scope"] = NULL;
    
    $token = $this->createAccessToken($client[0], $input["scope"], $stored["user_id"]);
    
    //TOKEN 为 数组 (包括 TOKEN 值, 有效时间, 权限)
    // $this->sendJsonHeaders();
    echo json_encode($token);
  }

  /**
   * Internal function used to get the client credentials from HTTP basic
   * auth or POST data.
   *
   * @return
   *   A list containing the client identifier and password, for example
   * @code
   * return array(
   *   $_POST["client_id"],
   *   $_POST["client_secret"],
   * );
   * @endcode
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-2
   *
   * @ingroup oauth2_section_2
   */
  protected function getClientCredentials() {
    
    if (isset($_SERVER["PHP_AUTH_USER"]) && $_POST && isset($_POST["client_id"]))
        $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_CLIENT);
    
    // Try basic auth
    if (isset($_SERVER["PHP_AUTH_USER"]))
        return array($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"]);
      
    // Try POST
    if ($_POST && isset($_POST["client_id"])) {
      if (isset($_POST["client_secret"]))
        return array($_POST["client_id"], $_POST["client_secret"]);

      return array($_POST["client_id"], NULL);
    }

    // No credentials were specified
    $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_CLIENT);
  }

  // End-user/client Authorization (Section 3 of IETF Draft).

  /**
   * Pull the authorization request data out of the HTTP request.
   *
   * @return
   *   The authorization parameters so the authorization server can prompt
   *   the user for approval if valid.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3
   *
   * @ingroup oauth2_section_3
   */
  public function getAuthorizeParams() {
    $filters = array(
      "client_id" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => OAUTH2_CLIENT_ID_REGEXP), "flags" => FILTER_REQUIRE_SCALAR),
      "response_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => OAUTH2_AUTH_RESPONSE_TYPE_REGEXP), "flags" => FILTER_REQUIRE_SCALAR),
      "redirect_uri" => array("filter" => FILTER_SANITIZE_URL),
      "state" => array("flags" => FILTER_REQUIRE_SCALAR),
      "scope" => array("flags" => FILTER_REQUIRE_SCALAR),
    );

    $input = filter_input_array(INPUT_GET, $filters);
    
    // Make sure a valid client id was supplied
    if (!$input["client_id"]) {
      if ($input["redirect_uri"])
        $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_CLIENT, NULL, NULL, $input["state"]);

      $this->errorJsonResponse(OAUTH2_HTTP_FOUND, OAUTH2_ERROR_INVALID_CLIENT); // We don't have a good URI to use
    }

    // redirect_uri is not required if already established via other channels
    // check an existing redirect URI against the one supplied
    $redirect_uri = $this->getRedirectUri($input["client_id"]);

    // At least one of: existing redirect URI or input redirect URI must be specified
    if (!$redirect_uri && !$input["redirect_uri"])
      $this->errorJsonResponse(OAUTH2_HTTP_FOUND, OAUTH2_ERROR_INVALID_REQUEST);

    // getRedirectUri() should return FALSE if the given client ID is invalid
    // this probably saves us from making a separate db call, and simplifies the method set
    if ($redirect_uri === FALSE)
      $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_CLIENT, NULL, NULL, $input["state"]);

    // If there's an existing uri and one from input, verify that they match
    if ($redirect_uri && $input["redirect_uri"]) {
      // Ensure that the input uri starts with the stored uri
      if (strcasecmp(substr($input["redirect_uri"], 0, strlen($redirect_uri)), $redirect_uri) !== 0)
        $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_REDIRECT_URI_MISMATCH, NULL, NULL, $input["state"]);
    }
    elseif ($redirect_uri) { // They did not provide a uri from input, so use the stored one
      $input["redirect_uri"] = $redirect_uri;
    }

    // type and client_id are required
    if (!$input["response_type"])
      $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_REQUEST, 'Invalid response type.', NULL, $input["state"]);

    // Check requested auth response type against the list of supported types
    if (array_search($input["response_type"], $this->getSupportedAuthResponseTypes()) === FALSE)
      $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE, NULL, NULL, $input["state"]);

    // Restrict clients to certain authorization response types
    if ($this->checkRestrictedAuthResponseType($input["client_id"], $input["response_type"]) === FALSE)
      $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_UNAUTHORIZED_CLIENT, NULL, NULL, $input["state"]);

    // Validate that the requested scope is supported
    if ($input["scope"] && !$this->checkScope($input["scope"], $this->getSupportedScopes()))
      $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_SCOPE, NULL, NULL, $input["state"]);
    
    return $input;
  }

  /**
   * Redirect the user appropriately after approval.
   *
   * After the user has approved or denied the access request the
   * authorization server should call this function to redirect the user
   * appropriately.
   *
   * @param $is_authorized
   *   TRUE or FALSE depending on whether the user authorized the access.
   * @param $params
   *   An associative array as below:
   *   - response_type: The requested response: an access token, an
   *     authorization code, or both.
   *   - client_id: The client identifier as described in Section 2.
   *   - redirect_uri: An absolute URI to which the authorization server
   *     will redirect the user-agent to when the end-user authorization
   *     step is completed.
   *   - scope: (optional) The scope of the access request expressed as a
   *     list of space-delimited strings.
   *   - state: (optional) An opaque value used by the client to maintain
   *     state between the request and callback.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3
   *
   * @ingroup oauth2_section_3
   */
  public function finishClientAuthorization($is_authorized, $params = array()) {
    $params += array(
      // 'scope' => NULL,
      'state' => NULL,
    );
    
    extract($params);
    
    if ($state !== NULL)
      $result["query"]["state"] = $state;

    if ($is_authorized === FALSE) {
      $result["query"]["error"] = OAUTH2_ERROR_USER_DENIED;
    }
    else {
      if ($response_type == OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE || $response_type == OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN)
        $result["query"]["code"] = $this->createAuthCode($client_id, $redirect_uri, $user_id, $scope);

      if ($response_type == OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN || $response_type == OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN)
        $result["fragment"] = $this->createAccessToken($client_id, $scope, $user_id);
    }
    
    $result['display'] = !empty($display) ? $display : '';

    $this->doRedirectUriCallback($redirect_uri, $result);
  }

  // Other/utility functions.

  /**
   * Redirect the user agent.
   *
   * Handle both redirect for success or error response.
   *
   * @param $redirect_uri
   *   An absolute URI to which the authorization server will redirect
   *   the user-agent to when the end-user authorization step is completed.
   * @param $params
   *   Parameters to be pass though buildUri().
   *
   * @ingroup oauth2_section_3
   */
  private function doRedirectUriCallback($redirect_uri, $params) {
    header("HTTP/1.1 ". OAUTH2_HTTP_FOUND);
    if($params['display'] == 'frame') {
        
        echo '<script>window.top.location.href="'.$this->buildUri($redirect_uri, $params).'";</script>';
        
    } else {
        header("Location: " . $this->buildUri($redirect_uri, $params));
    }
    exit;
  }

  /**
   * Build the absolute URI based on supplied URI and parameters.
   *
   * @param $uri
   *   An absolute URI.
   * @param $params
   *   Parameters to be append as GET.
   *
   * @return
   *   An absolute URI with supplied parameters.
   *
   * @ingroup oauth2_section_3
   */
  private function buildUri($uri, $params) {
    session_start();
    $parse_url = parse_url($uri);

    // Add our params to the parsed uri
    foreach ($params as $k => $v) {
      if (isset($parse_url[$k]))
        $parse_url[$k] .= "&" . http_build_query($v);
      else
        $parse_url[$k] = http_build_query($v);
    }

    // Put humpty dumpty back together
    $return =
      ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
      . ((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "")
      . ((isset($parse_url["host"])) ? $parse_url["host"] : "")
      . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
      . ((isset($parse_url["path"])) ? $parse_url["path"] : "")
      . ((isset($parse_url["query"])) ? "?" . $parse_url["query"] : "")
      . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "") . "&ssid=".base64_encode('2egc83'.session_id());
    return $return;
  }

  /**
   * Handle the creation of access token, also issue refresh token if support.
   *
   * This belongs in a separate factory, but to keep it simple, I'm just
   * keeping it here.
   *
   * @param $client_id
   *   Client identifier related to the access token.
   * @param $scope
   *   (optional) Scopes to be stored in space-separated string.
   *
   * @ingroup oauth2_section_4
   */
  protected function createAccessToken($client_id, $scope = NULL, $user_id = 10) {
    $token = array(
      "access_token" => $this->genAccessToken(),
      "expires_in" => $this->getVariable('access_token_lifetime', OAUTH2_DEFAULT_ACCESS_TOKEN_LIFETIME),
      "user_id" => $user_id,
      "scope" => $scope
    );
    $refresh_token = '';
    // Issue a refresh token also, if we support them
    if (in_array(OAUTH2_GRANT_TYPE_REFRESH_TOKEN, $this->getSupportedGrantTypes())) {
      $refresh_token = $this->genAccessToken();
      // $this->setRefreshToken($token["refresh_token"], $client_id, time() + $this->getVariable('refresh_token_lifetime', OAUTH2_DEFAULT_REFRESH_TOKEN_LIFETIME), $user_id, $scope);
      $token["refresh_token"] = $refresh_token;
      // If we've granted a new refresh token, expire the old one
      if ($this->getVariable('_old_refresh_token'))
        $this->unsetRefreshToken($this->getVariable('_old_refresh_token'));
    }
    $access_token = $token['access_token'];

    $this->setAccessToken($access_token, $client_id, time() + $this->getVariable('access_token_lifetime', OAUTH2_DEFAULT_ACCESS_TOKEN_LIFETIME), $token['user_id'], $scope, $refresh_token);

    return $token;
  }

  /**
   * Handle the creation of auth code.
   *
   * This belongs in a separate factory, but to keep it simple, I'm just
   * keeping it here.
   *
   * @param $client_id
   *   Client identifier related to the access token.
   * @param $redirect_uri
   *   An absolute URI to which the authorization server will redirect the
   *   user-agent to when the end-user authorization step is completed.
   * @param $scope
   *   (optional) Scopes to be stored in space-separated string.
   *
   * @ingroup oauth2_section_3
   */
  private function createAuthCode($client_id, $redirect_uri, $userId, $scope = NULL) {
    $code = $this->genAuthCode();
    $this->setAuthCode($code, $client_id, $redirect_uri, time() + $this->getVariable('auth_code_lifetime', OAUTH2_DEFAULT_AUTH_CODE_LIFETIME), $userId, $scope);
    return $code;
  }

  /**
   * Generate unique access token.
   *
   * Implementing classes may want to override these function to implement
   * other access token or auth code generation schemes.
   *
   * @return
   *   An unique access token.
   *
   * @ingroup oauth2_section_4
   */
  protected function genAccessToken() {
    return md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid())));
  }

  /**
   * Generate unique auth code.
   *
   * Implementing classes may want to override these function to implement
   * other access token or auth code generation schemes.
   *
   * @return
   *   An unique auth code.
   *
   * @ingroup oauth2_section_3
   */
  protected function genAuthCode() {
    return md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid())));
  }

  /**
   * Pull out the Authorization HTTP header and return it.
   *
   * Implementing classes may need to override this function for use on
   * non-Apache web servers.
   *
   * @return
   *   The Authorization HTTP header, and FALSE if does not exist.
   *
   * @todo Handle Authorization HTTP header for non-Apache web servers.
   *
   * @ingroup oauth2_section_5
   */
  private function getAuthorizationHeader() {
    if (array_key_exists("HTTP_AUTHORIZATION", $_SERVER))
      return $_SERVER["HTTP_AUTHORIZATION"];

    if (function_exists("apache_request_headers")) {
      $headers = apache_request_headers();

      if (array_key_exists("Authorization", $headers))
        return $headers["Authorization"];
    }
    
    return FALSE;
  }

  /**
   * Send out HTTP headers for JSON.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.2
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3
   *
   * @ingroup oauth2_section_4
   */
  private function sendJsonHeaders() {
    header("Content-Type: application/json");
    header("Cache-Control: no-store");
  }

  /**
   * Redirect the end-user's user agent with error message.
   *
   * @param $redirect_uri
   *   An absolute URI to which the authorization server will redirect the
   *   user-agent to when the end-user authorization step is completed.
   * @param $error
   *   A single error code as described in Section 3.2.1.
   * @param $error_description
   *   (optional) A human-readable text providing additional information,
   *   used to assist in the understanding and resolution of the error
   *   occurred.
   * @param $error_uri
   *   (optional) A URI identifying a human-readable web page with
   *   information about the error, used to provide the end-user with
   *   additional information about the error.
   * @param $state
   *   (optional) REQUIRED if the "state" parameter was present in the client
   *   authorization request. Set to the exact value received from the client.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2
   *
   * @ingroup oauth2_error
   */
  private function errorDoRedirectUriCallback($redirect_uri, $error, $error_description = NULL, $error_uri = NULL, $state = NULL) {
    $result["query"]["error"] = $error;

    if ($state)
      $result["query"]["state"] = $state;

    if ($this->getVariable('display_error') && $error_description)
      $result["query"]["error_description"] = $error_description;

    if ($this->getVariable('display_error') && $error_uri)
      $result["query"]["error_uri"] = $error_uri;

    $this->doRedirectUriCallback($redirect_uri, $result);
  }

  /**
   * Send out error message in JSON.
   *
   * @param $http_status_code
   *   HTTP status code message as predefined.
   * @param $error
   *   A single error code.
   * @param $error_description
   *   (optional) A human-readable text providing additional information,
   *   used to assist in the understanding and resolution of the error
   *   occurred.
   * @param $error_uri
   *   (optional) A URI identifying a human-readable web page with
   *   information about the error, used to provide the end-user with
   *   additional information about the error.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3
   *
   * @ingroup oauth2_error
   */
  private function errorJsonResponse($http_status_code, $error, $error_description = NULL, $error_uri = NULL) {
    $result['error'] = $error;

    if ($this->getVariable('display_error') && $error_description)
      $result["error_description"] = $error_description;

    if ($this->getVariable('display_error') && $error_uri)
      $result["error_uri"] = $error_uri;

    header("HTTP/1.1 " . $http_status_code);
    $this->sendJsonHeaders();
    echo json_encode($result);

    exit;
  }

  /**
   * Send a 401 unauthorized header with the given realm and an error, if
   * provided.
   *
   * @param $http_status_code
   *   HTTP status code message as predefined.
   * @param $realm
   *   The "realm" attribute is used to provide the protected resources
   *   partition as defined by [RFC2617].
   * @param $scope
   *   A space-delimited list of scope values indicating the required scope
   *   of the access token for accessing the requested resource.
   * @param $error
   *   The "error" attribute is used to provide the client with the reason
   *   why the access request was declined.
   * @param $error_description
   *   (optional) The "error_description" attribute provides a human-readable text
   *   containing additional information, used to assist in the understanding
   *   and resolution of the error occurred.
   * @param $error_uri
   *   (optional) The "error_uri" attribute provides a URI identifying a human-readable
   *   web page with information about the error, used to offer the end-user
   *   with additional information about the error. If the value is not an
   *   absolute URI, it is relative to the URI of the requested protected
   *   resource.
   *
   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2
   *
   * @ingroup oauth2_error
   */
  private function errorWWWAuthenticateResponseHeader($http_status_code, $realm, $error, $error_description = NULL, $error_uri = NULL, $scope = NULL) {
    
    $realm = $realm === NULL ? $this->getDefaultAuthenticationRealm() : $realm;
    $result = "WWW-Authenticate: OAuth realm='" . $realm . "'";

    if ($error)
      $result .= ", error='" . $error . "'";

    if ($this->getVariable('display_error') && $error_description)
      $result .= ", error_description='" . $error_description . "'";

    if ($this->getVariable('display_error') && $error_uri)
      $result .= ", error_uri='" . $error_uri . "'";

    if ($scope)
      $result .= ", scope='" . $scope . "'";
    
    header("HTTP/1.1 ". $http_status_code);
    header($result);

    exit;
  }
}

代码比较多。有兴趣的可以细看。

由于采用Vendor方法引入,所以这里的文件都没有采用命名空间。

5-3、在同目录下,建ThinkOAuth2.php文件,这个文件里的类继承上面那个基类:

<?php
require "oauth2.class.php";
class ThinkOAuth2 extends OAuth2{
    private $db;
    private $table;
 
    /**
     * 构造
     */
    public function __construct() {
        parent::__construct();
        $this -> db = M();
        $this -> table = array(
            'auth_codes'=>C('OAUTH2_CODES_TABLE'),
            'clients'=>C('OAUTH2_CLIENTS_TABLE'),
            'tokens'=>C('OAUTH2_TOKEN_TABLE')
        );
    }
 
    /**
     * 析构
     */
    function __destruct() {
        $this->db = NULL; // Release db connection
    }
 
    private function handleException($e) {
        echo "Database error: " . $e->getMessage();
        exit;
    }
 
    /**
     *
     * 增加client
     * @param string $client_id
     * @param string $client_secret
     * @param string $redirect_uri
     */
    public function addClient($client_id, $client_secret, $redirect_uri) {
 
        $time = time();
        $sql = "INSERT INTO {$this -> table['clients']} (client_id, client_secret, redirect_uri,create_time) VALUES ('{$client_id}', '{$client_secret}', '{$redirect_uri}','{$time}')";
        $res = $this -> db -> execute($sql);
        return res;    
        
 
    }
 
    /**
     * Implements OAuth2::checkClientCredentials()
     * @see OAuth2::checkClientCredentials()
     */
    protected function checkClientCredentials($client_id, $client_secret = NULL) {
 
        $sql = "SELECT client_secret FROM {$this -> table['clients']} WHERE client_id = {$client_id}";
 
        $result = $this -> db -> query($sql);
        if ($client_secret === NULL) {
            return $result !== FALSE;
        }
 
        //Log::write("checkClientCredentials : ".$result);
        //Log::write("checkClientCredentials : ".$result[0]);
        //Log::write("checkClientCredentials : ".$result[0]["client_secret"]);
 
        return $result[0]["client_secret"] == $client_secret;
 
    }
 
    /**
     * Implements OAuth2::getRedirectUri().
     * @see OAuth2::getRedirectUri()
     */
    protected function getRedirectUri($client_id) {
 
        $sql = "SELECT redirect_uri FROM {$this -> table['clients']} WHERE client_id = {$client_id}";
 
        $result = $this -> db -> query($sql);
 
        if ($result === FALSE) {
            return FALSE;
        }
 
        //Log::write("getRedirectUri : ".$result);
        //Log::write("getRedirectUri : ".$result[0]);
        //Log::write("getRedirectUri : ".$result[0]["redirect_uri"]);
 
        return isset($result[0]["redirect_uri"]) && $result[0]["redirect_uri"] ? $result[0]["redirect_uri"] : NULL;
 
    }
 
    /**
     * Implements OAuth2::getAccessToken().
     * @see OAuth2::getAccessToken()
     */
    protected function getAccessToken($access_token) {
 
        $sql = "SELECT client_id, expires_in, scope FROM {$this -> table['tokens']} WHERE access_token = '{$access_token}'";
 
        $result = $this -> db -> query($sql);
 
        //Log::write("getAccessToken : ".$result);
        //Log::write("getAccessToken : ".$result[0]);
 
        return $result !== FALSE ? $result : NULL;
 
    }
 
    /**
     * Implements OAuth2::setAccessToken().
     * @see OAuth2::setAccessToken()
     */
    protected function setAccessToken($access_token, $client_id, $expires, $user_id, $scope=NULL, $refresh_token='') {
 
$sql = "INSERT INTO {$this -> table['tokens']} (access_token, client_id, expires_in, scope,user_id,refresh_token) VALUES ('{$access_token}', '{$client_id}', '{$expires}', '{$scope}',{$user_id},'{$refresh_token}')";
        $this -> db -> execute($sql);
 
    }
 
    /**
     * Overrides OAuth2::getSupportedGrantTypes().
     * @see OAuth2::getSupportedGrantTypes()
     */
    protected function getSupportedGrantTypes() {
        return array(
            OAUTH2_GRANT_TYPE_AUTH_CODE
        );
    }
 
    /**
     * Overrides OAuth2::getAuthCode().
     * @see OAuth2::getAuthCode()
     */
    protected function getAuthCode($code) {
 
        $sql = "SELECT user_id, code, client_id, redirect_uri, expires, scope FROM {$this -> table['auth_codes']} WHERE code = '{$code}'";
        $result = $this -> db -> query($sql);
        
        //Log::write("getAuthcode : ".$result);
        //Log::write("getAuthcode : ".$result[0]);
        //Log::write("getAuthcode : ".$result[0]["code"]);
 
        return $result !== FALSE ? $result[0] : NULL;
 
    }
 
    /**
     * Overrides OAuth2::setAuthCode().
     * @see OAuth2::setAuthCode()
     */
    protected function setAuthCode($code, $client_id, $redirect_uri, $expires, $scope = NULL) {
 
        $time = time();
        $sql = "INSERT INTO {$this -> table['auth_codes']} (code, client_id, redirect_uri, expires, scope) VALUES ('{$code}', '{$client_id}', '{$redirect_uri}', '{$expires}', '{$scope}')"; 
        $result = $this -> db -> execute($sql);
  }
 
  /**
   * Overrides OAuth2::checkUserCredentials().
   * @see OAuth2::checkUserCredentials()
   */
  protected function checkUserCredentials($client_id, $username, $password){
      return TRUE;
  }
  

}

这里涉及到一些数据库的操作。继承出来操作,还是比较一目了然的。

5-4、准备好类了。我们就可以实现控制器里的功能代码。动手之前先搞清楚我们的意图。

首先,我们需要分配一个oauth_client和一个oauth_secrete给第三方网站(也就是client.ruanwenwu.cn)来获取oauth服务。

其次,client.ruanwenwu.cn会把用户导入到server.ruanwenwu.cn来进行授权。所以我们我们还需要准备授权页面。

因为涉及到服务端和客户端的数据交互,所以可能服务端和客户端的相关介绍我会交叉进行。

5-4-1、创建oauth_client。

5-4-2、获取oauth_cliest列表。

5-4-3、准备获取用户授权的页面和逻辑。(授权服务器)

5-4-4、准备提供用户数据的逻辑。(资源服务器)

这些逻辑我都写在一个控制器OauthController.class.php里。代码如下:

<?php
namespace Home\Controller;

use Think\Controller;

class OauthController extends Controller{
    private $oauth = NULL;
 
    function _initialize(){
 
        header("Content-Type: application/json");
        Vendor("oauth.ThinkOAuth2");
        $this -> oauth = new \ThinkOAuth2();
 
    }
 
    public function index(){
 
        header("Content-Type:application/json; charset=utf-8");
        $this -> ajaxReturn(null, 'oauth-server-start', 1, 'json');
 
    }
 
    public function access_token() {
 
        $this -> oauth -> grantAccessToken();
 
    }
 
    //权限验证
    public function authorize() {
 
        if (IS_POST) {
            
            if(true){
                //这个地方验证成功后要把用户的uid加进来
                $_POST['user_id'] = 1;
            $this -> oauth -> finishClientAuthorization(true, $_POST);    //第一个参数很重要,是用户是否授权
            }
            return;
        }
 
        ///表单准备
        $auth_params = $this -> oauth -> getAuthorizeParams();
        $this->assign("auth_params",$auth_params);
        $this->display();
 
    }
 
    public function addclient() {
        echo 'jack';
        if (IS_POST) {
            echo 'pd';
            $res = $this -> oauth -> addClient($_POST["client_id"], $_POST["client_secret"], $_POST["redirect_uri"]);
            if($res){
                $this->redirect("/home/Oauth/clientlist");
                die;
            }
        }
 
        $this->display();
    }
    
    public function clientlist(){
        $res = M()->table("oauth_client")->select();
        $this->assign("clients",$res);
        var_dump($res);
        $this->display();
    }
}

顺便我将用到的模板文件(server.ruanwenwu.cn\Application\Home\View\oauth目录下)也贴出来给大家参考一下:

addclient.html

<!doctype html>
    <head>
        <meta charset="utf-8" />
        <title>这里是添加客户端界面</title>
    </head>
    <body>
        <h1>添加客户端</h1>
        <form action="" method="post" >
            clientId:<input type="text" name="client_id" />
            <br />
            client_secrete:<input type="text" name="client_secret" />
            <br />
            callbackUrl:<input type="text" name="redirect_uri" />
            <br />
            <input type="submit" value="submit" />
            
        </form>
    </body>
</html>

authorize.html

<!doctype html>
<html>
    <form action="" method="post">
        用户名:<input type="text" name="username" /> 
        <br />
        密码:<input type="text" name="pwd" />
        <input type="hidden" name="response_type" value="code" />
        <input type="hidden" name="client_id" value="{$auth_params['client_id']}" />
        <input type="hidden" name="redirect_uri" value="{$auth_params['redirect_uri']}" />
        <input type="submit" value="submit" />
    </form>
</html>

注意:我这里用到了几个隐藏域。这个是必须的,在post提交验证时会用到。

5-4-5、创建oauth2.0用户。

打 开server.ruanwenwu.cn/index.php/home/oauth/addcient,输入client_id和 client_secrete还有redirect_uri创建一个oauth2.0用户。其中redirect_uri是用户授权后需要跳转的地方。

到这里服务端的部署就差不多结束了。现在开始部署客户端。

三、oauth2.0客户端搭建。

1、同服务端一样搭建client.ruanwenwu.cn。(也是用thinkphp)。

2、能正常访问后。在类库中加入oauth2.0客户端类。

在\client.ruanwenwu.cn\ThinkPHP\Library\Org\Util目录下建立Oauth2.class.php,代码如下:

<?php
namespace Org\Util;
/**
 * 与 OAuth2 服务器 通讯验证 核心类 (PHP版)
 * 
 * @description OAuth2 说明请大家参考 木蚂蚁接口文档
 *
 *
 * @author   Mumayi-Team (zhaopan赵攀)
 * @version  1.0
 * @datetime 2013-12-31
 */
class Oauth2
{
    /*
     * 以下几个参数 具体作用和用法 详见 木蚂蚁接口文档
     */
    
    //分配的客户端 ID
    public $client_id;
    
    //分配的客户端 密匙
    public $client_secret;
    
    //获得的 用户授权 访问令牌
    public $access_token;
    
    //刷新 访问令牌 (过期时使用)
    public $refresh_token;
    
    
    // 最后一个收到的HTTP代码 (用于调试,忽略)
    public $http_code;
    
    //预留字段 (忽略)
    public $url;
    
    //设置 基础链接
    public $host = "http://server.ruanwenwu.cn/index.php/home/oauth/";
    
    //请求超时时间
    public $timeout = 30;
    
    //链接超时时间
    public $connecttimeout = 30;
    
    //是否验证 SSL 证书
    public $ssl_verifypeer = FALSE;
    
    //请求结果处理格式
    public $format = 'json';
    
    //以 JSON_DECODE 方式解析
    public $decode_json = TRUE;
    
    //预留字段 (忽略)
    public $http_info;
    
    //CURL 函数使用 (验证使用, 忽略)
    public $useragent = 'Mumayi Team OAuth2 v1.0';

    //是否 开启 请求调试 (开启时 将输出 详细的请求结果)
    public $debug = FALSE;

    //边界 (CURL使用 写入在Header的分界线, 忽略)
    public static $boundary = '';

    //返回 OAuth 令牌 请求地址
    function accessTokenURL()  { return $this->host.'access_token'; }
    
    //返回 OAuth 自动授权 请求地址
    function authorizeURL()    { return $this->host.'authorize'; }

    //构造函数 赋值 一些必要参数
    function __construct($client_id, $client_secret, $access_token = NULL, $refresh_token = NULL) {
        $this->client_id = $client_id;
        $this->client_secret = $client_secret;
        $this->access_token = $access_token;
        $this->refresh_token = $refresh_token; 
        // $this->debug = true;
    }

    /**
     * authorize接口
     *
     * 对应API:{@link http://u.mumayi.com/oauth/authorize Oauth2/authorize}
     *
     * @param string $url 授权后的回调地址,站外应用需与回调地址一致,站内应用需要填写canvas page的地址
     * @param string $response_type 支持的值包括 code 和token 默认值为code
     * @param string $state 用于保持请求和回调的状态。在回调时,会在Query Parameter中回传该参数
     * @param string $display 授权页面类型 可选范围: 
     *  - default        默认授权页面
     *  - mobile        支持html5的手机
     *  - popup            弹窗授权页
     *  - wap1.2        wap1.2页面
     *  - wap2.0        wap2.0页面
     *  - js            js-sdk 专用 授权页面是弹窗,返回结果为js-sdk回掉函数
     *  - apponweibo    站内应用专用,站内应用不传display参数,并且response_type为token时,默认使用改display.授权后不会返回access_token,只是输出js刷新站内应用父框架
     * @return array
     */
    function getAuthorizeURL( $url, $response_type = 'code', $state = NULL, $display = NULL ) {
        $params = array();
        $params['client_id'] = $this->client_id;
        $params['redirect_uri'] = $url;
        $params['response_type'] = $response_type;
        $params['state'] = $state;
        $params['display'] = $display;
        return $this->authorizeURL() . "&" . http_build_query($params);
    }

    /**
     * access_token接口
     *
     * 对应API:{@link http://open.weibo.com/wiki/OAuth2/access_token OAuth2/access_token}
     *
     * @param string $type 请求的类型,可以为:code, password, token
     * @param array $keys 其他参数:
     *  - 当$type为code时: array('code'=>..., 'redirect_uri'=>...)
     *  - 当$type为password时: array('username'=>..., 'password'=>...)
     *  - 当$type为token时: array('refresh_token'=>...)
     * @return array
     */
    function getAccessToken( $type = 'code', $keys ) {
        $params = array();
        $params['client_id'] = $this->client_id;
        $params['client_secret'] = $this->client_secret;
        if ( $type === 'token' ) {
            $params['grant_type'] = 'refresh_token';
            $params['refresh_token'] = $keys['refresh_token'];
        } elseif ( $type === 'code' ) {
            $params['grant_type'] = 'authorization_code';
            $params['code'] = $keys['code'];
            $params['redirect_uri'] = $keys['redirect_uri'];
        } elseif ( $type === 'password' ) {
            $params['grant_type'] = 'password';
            $params['username'] = $keys['username'];
            $params['password'] = $keys['password'];
        } else {
            echo json_encode(array('error' => '错误的授权类型['.$type.'(token, code)]'));
            die;
        }
        
        $response = $this->oAuthRequest($this->accessTokenURL(), 'POST', $params);
        //var_dump($response);die;
        $token = json_decode($response, true);
        
        if ( is_array($token) && !isset($token['error']) ) {
            $this->access_token = $token['access_token'];
            //$this->refresh_token = $token['refresh_token'];
        } else {
            echo json_encode(array('error' => '获取访问令牌失败。['.$token['error'].']'));
            die;
        }
        return $token;
    }

    /**
     * 解析 signed_request
     *
     * @param string $signed_request 应用框架在加载iframe时会通过向Canvas URL post的参数signed_request
     *
     * @return array
     */
    function parseSignedRequest($signed_request) {
        list($encoded_sig, $payload) = explode('.', $signed_request, 2); 
        $sig = self::base64decode($encoded_sig) ;
        $data = json_decode(self::base64decode($payload), true);
        if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') return '-1';
        $expected_sig = hash_hmac('sha256', $payload, $this->client_secret, true);
        return ($sig !== $expected_sig)? '-2':$data;
    }

    /**
     * @ignore
     */
    function base64decode($str) {
        return base64_decode(strtr($str.str_repeat('=', (4 - strlen($str) % 4)), '-_', '+/'));
    }

    /**
     * 读取jssdk授权信息,用于和jssdk的同步登录
     *
     * @return array 成功返回array('access_token'=>'value', 'refresh_token'=>'value'); 失败返回false
     */
    function getTokenFromJSSDK() {
        $key = "mumayijs_" . $this->client_id;
        if ( isset($_COOKIE[$key]) && $cookie = $_COOKIE[$key] ) {
            parse_str($cookie, $token);
            if ( isset($token['access_token']) && isset($token['refresh_token']) ) {
                $this->access_token = $token['access_token'];
                $this->refresh_token = $token['refresh_token'];
                return $token;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * 从数组中读取access_token和refresh_token
     * 常用于从Session或Cookie中读取token,或通过Session/Cookie中是否存有token判断登录状态。
     *
     * @param array $arr 存有access_token和secret_token的数组
     * @return array 成功返回array('access_token'=>'value', 'refresh_token'=>'value'); 失败返回false
     */
    function getTokenFromArray( $arr ) {
        if (isset($arr['access_token']) && $arr['access_token']) {
            $token = array();
            $this->access_token = $token['access_token'] = $arr['access_token'];
            if (isset($arr['refresh_token']) && $arr['refresh_token']) {
                $this->refresh_token = $token['refresh_token'] = $arr['refresh_token'];
            }

            return $token;
        } else {
            return false;
        }
    }

    /**
     * 以 GET 请求方式 请求 OAuth 验证服务器
     *
     * @return mixed
     */
    function get($url, $parameters = array()) {
        $response = $this->oAuthRequest($url, 'GET', $parameters);
        if ($this->format === 'json' && $this->decode_json) {
            return json_decode($response, true);
        }
        return $response;
    }

    /**
     * 以 POST 请求方式 请求 OAuth 验证服务器
     *
     * @return mixed
     */
    function post($url, $parameters = array(), $multi = false) {
        $response = $this->oAuthRequest($url, 'POST', $parameters, $multi );
        if ($this->format === 'json' && $this->decode_json) {
            return json_decode($response, true);
        }
        return $response;
    }

    /**
     * 请求 OAuth 验证服务器 进行验证
     *
     * @return string
     */
    function oAuthRequest($url, $method, $parameters, $multi = false) {

        if (strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0) {
            $url = "{$this->host}{$url}.{$this->format}";
        }

        switch ($method) {
            case 'GET':
                if(!empty($parameters))
                    $url = $url . '&' . http_build_query($parameters);

                return $this->http($url, 'GET');
            default:
                $headers = array();
                if (!$multi && (is_array($parameters) || is_object($parameters)) ) {
                    $body = http_build_query($parameters);
                } else {
                    $body = self::build_http_query_multi($parameters);
                    $headers[] = "Content-Type: multipart/form-data; boundary=" . self::$boundary;
                }
                
                //return $this->http($url, $method, $body, $headers);
                return $this->http($url, $method, $body, $headers);
        }
    }

    /**
     * 使用 CURL 模拟一个 HTTP 请求 
     *
     * @description (需要开启 CURL 扩展)
     * 
     * @return string API results
     * 
     */
    function http($url, $method, $postfields = NULL, $headers = array()) {
        $this->http_info = array();
        $ci = curl_init();
        /* Curl settings */
        curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
        curl_setopt($ci, CURLOPT_USERAGENT, $this->useragent);
        curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, $this->connecttimeout);
        curl_setopt($ci, CURLOPT_TIMEOUT, $this->timeout);
        curl_setopt($ci, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ci, CURLOPT_ENCODING, "");
        curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, $this->ssl_verifypeer);
        curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, 1);
        curl_setopt($ci, CURLOPT_HEADERFUNCTION, array($this, 'getHeader'));
        curl_setopt($ci, CURLOPT_HEADER, FALSE);

        switch ($method) {
            case 'POST':
                curl_setopt($ci, CURLOPT_POST, TRUE);
                if (!empty($postfields)) {
                    curl_setopt($ci, CURLOPT_POSTFIELDS, $postfields);
                    $this->postdata = $postfields;
                }
                break;
            case 'DELETE':
                curl_setopt($ci, CURLOPT_CUSTOMREQUEST, 'DELETE');
                if (!empty($postfields)) {
                    $url = "{$url}?{$postfields}";
                }
        }

        if ( isset($this->access_token) && $this->access_token )
            $headers[] = "Authorization: OAuth2 ".$this->access_token;

        if ( !empty($this->remote_ip) ) {
            if ( defined('SAE_ACCESSKEY') ) {
                $headers[] = "SaeRemoteIP: " . $this->remote_ip;
            } else {
                $headers[] = "API-RemoteIP: " . $this->remote_ip;
            }
        } else {
            if ( !defined('SAE_ACCESSKEY') ) {
                $headers[] = "API-RemoteIP: " . $_SERVER['REMOTE_ADDR'];
            }
        }
        curl_setopt($ci, CURLOPT_URL, $url );
        curl_setopt($ci, CURLOPT_HTTPHEADER, $headers );
        curl_setopt($ci, CURLINFO_HEADER_OUT, TRUE );

        $response = curl_exec($ci);
        $this->http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
        $this->http_info = array_merge($this->http_info, curl_getinfo($ci));
        $this->url = $url;
        
        if ($this->debug) {
            echo "=====post data======\r\n";
            var_dump($postfields);

            echo "=====headers======\r\n";
            print_r($headers);

            echo '=====request info====='."\r\n";
            print_r( curl_getinfo($ci) );

            echo '=====response====='."\r\n";
            print_r( $response );
        }
        curl_close ($ci);
        return $response;
    }

    /**
     * 从结果中 获取 header 参数 (CURL 使用)
     *
     * @return int
     * 
     * @ignore (忽略)
     */
    function getHeader($ch, $header) {
        $i = strpos($header, ':');
        if (!empty($i)) {
            $key = str_replace('-', '_', strtolower(substr($header, 0, $i)));
            $value = trim(substr($header, $i + 2));
            $this->http_header[$key] = $value;
        }
        return strlen($header);
    }

    /**
     * 把要传递的参数 重新构造一下 (多维数组)
     * 
     * @ignore (忽略)
     */
    public static function build_http_query_multi($params) {
        if (!$params) return '';

        uksort($params, 'strcmp');

        $pairs = array();

        self::$boundary = $boundary = uniqid('------------------');
        $MPboundary = '--'.$boundary;
        $endMPboundary = $MPboundary. '--';
        $multipartbody = '';

        foreach ($params as $parameter => $value) {

            if( in_array($parameter, array('pic', 'image')) && $value{0} == '@' ) {
                $url = ltrim( $value, '@' );
                $content = file_get_contents( $url );
                $array = explode( '?', basename( $url ) );
                $filename = $array[0];

                $multipartbody .= $MPboundary . "\r\n";
                $multipartbody .= 'Content-Disposition: form-data; name="' . $parameter . '"; filename="' . $filename . '"'. "\r\n";
                $multipartbody .= "Content-Type: image/unknown\r\n\r\n";
                $multipartbody .= $content. "\r\n";
            } else {
                $multipartbody .= $MPboundary . "\r\n";
                $multipartbody .= 'content-disposition: form-data; name="' . $parameter . "\"\r\n\r\n";
                $multipartbody .= $value."\r\n";
            }

        }

        $multipartbody .= $endMPboundary;
        return $multipartbody;
    }
    
      public function get_uid() {
        $hostUrl = 'http://server.ruanwenwu.cn/index.php/home/resource/userinfo?type=mumayi';
        $params = array(
            'access_token' => $this->access_token,
        );
        return $this->get( $hostUrl, $params );
     }
}

注意:这个类的加载跟服务端采用的不同的方式。这里用了命名空间。服务端用的vendor。

3、准备访问链接。

我们通过这个url将用户导向server.ruanwenwu.cn进行授权,发送accesss_code操作。

我在client.ruanwenwu.cn的index控制器中实现的这个url。控制器里没有逻辑,我只贴index.html了:

<!doctype html>
<html>
    <head></head>
    <body>
        <a href="http://server.ruanwenwu.cn/index.php/home/oauth/authorize?redirect_uri=http://client.ruanwenwu.cn/index.php/home/login&response_type=code&client_id=10009174&type=123456">登陆</a>
    </body>
</html>

注 意:这个url中redirect_uri,response_type为必选参数。而且这个redirect_uri必须包含我们在服务端添加 oauth服务时写的那个redirect_uri。否则不能通过验证。(也就是说:在我们这个例子里,这个redirect_uri必须包含:

http://client.ruanwenwu.cn/index.php/home/login

4、点击链接,跳转到服务端对应的url。实际上就是server.ruanwenwu.cn的authorize操作。在这里会对用户进行验证,并判断是否授权。

    //权限验证
    public function authorize() {
 
        if (IS_POST) {
            
            if(true){
                //这个地方验证成功后要把用户的uid加进来
                $_POST['user_id'] = 1;    //这里是写死的,实际上是从数据库读出来
            $this -> oauth -> finishClientAuthorization(true, $_POST);    //第一个参数很重要,是用户是否授权
            }
            return;
        }
 
        ///表单准备
        $auth_params = $this -> oauth -> getAuthorizeParams();
        $this->assign("auth_params",$auth_params);
        $this->display();
 
    }

注意看我在这个操作里写的注释。

验证成功后,页面又跳回到我们之前写的redirect_uri,并带上了code参数,这就是验证服务器向客户端发送的access_code。

5、客户端拿到access_code,向验证服务器请求access_token。

在我的client.ruanwenwu.cn的loginController.class.php中进行:

<?php
namespace Home\Controller;

use Think\Controller;
use Org\Util\Oauth2;
class LoginController extends Controller{
    public function _initialize(){

    }
    
    public function index(){
        $this->oauth = new Oauth2('10009174','123456');
        $keys = array();
        //keys 数组 赋值 code 值 (换取token, 必须参数)
        $keys['code'] = $_GET['code'];
        //keys 数组 赋值 回调地址信息 (换取token, 必须参数)
        $keys['redirect_uri'] = 'http://client.ruanwenwu.cn/index.php/home/login?type=mumayidev';
        //根据 code 获取 token
        //var_dump( $token = $this->oauth->getAccessToken( 'code', $keys )) ;
        $token = $this->oauth->getAccessToken( 'code', $keys ) ;
        
        //现在已经得到token,并且将access_token写到对象里了。就可以请求资源了
        var_dump( $this->oauth->get_uid());
        die;
    }
}
   $token = $this->oauth->getAccessToken( 'code', $keys ) ;    //这就是请求代码。code是验证方式。$keys数组里要code和redirect_uri。在服务器端需要验证

服务端的响应是这样的:

验证client.ruanwenwu.cn的client_id和secrete是否合法,还要判读access_code是否过期

    public function access_token() {
 
        $this -> oauth -> grantAccessToken();    //这个方法里有所有的验证及生成access_token的操作。(会有数据库的写入)
 
    }

服务端生成access_token之后,会返回给客户端。

客户端拿到access_token之后,向资源服务器请求用户资源:

在这里我的验证服务器和资源服务器都是server.ruanwenwu.cn。我用resourceController.class.php来做资源控制:

<?php
namespace Home\Controller;

use Think\Controller;

class ResourceController extends Controller{
    protected $db;
    public function _initialize(){
        $this->db = M();
    }
    public function userinfo(){
        // $arr = array("username"=>"阮文武","sex"=>1,"img"=>"http://weewrerfdf");
                 // echo json_encode($arr);
                 // die;
         if(isset($_GET['access_token']) && $_GET['access_token']){
              $res = $this->db->table("oauth_token")->where("access_token = '".$_GET['access_token']."'")->find();
             if($res && count($res) > 0){
                 if($res['expires_in'] > time()){
                 $arr = array("username"=>"阮文武d","sex"=>1,"img"=>"http://weewrerfdf");
                 echo json_encode($arr);
                 die;
                 }
             }
         };
    }
}

如果access_token有效,就查到用户id,取出用户的信息。我这里只是做测试,所以就直接自己返回了数据。原本写的很详细,后来因为内容字段是text,没保存完整,这会时间比较赶,就写的简单一点。

再抽时间补充吧。

欢迎大家与我讨论哦。

 

 

http://www.tuicool.com/articles/u6beUju

posted @ 2017-01-10 16:53  fleam  阅读(1386)  评论(0编辑  收藏  举报