用Yii2建立Oauth2 server

建立一個yii2專案,然後安裝這個extension

底下為Yii2設定的部份,是設定一個 module以及在bootstrap階段就執行oauth2 module,和extension的說明有些不太一樣。

'bootstrap' => ['oauth2'],
'components'=>[
     'urlManager' => [
          'enablePrettyUrl' => true,
          'showScriptName' => false,
          'rules' => [
              'POST oauth2/<action:(.+)>' => 'oauth2/rest/<action>',
          ],
      ],
],
'modules' => [
    'oauth2' => [
        'class' => 'filsh\yii2\oauth2server\Module',
        'tokenParamName' => 'access_token',
        'tokenAccessLifetime' => 3600 * 24,
        'storageMap' => [
            'user_credentials' => 'app\models\OauthUsers',
            'user_claims'=>  'app\models\OauthUsers',
        ],
        'grantTypes' => [
            'user_credentials' => [
                'class' => 'OAuth2\GrantType\UserCredentials',
            ],
            'authorization_code' => [
                'class' => 'OAuth2\GrantType\AuthorizationCode',
            ],
            'refresh_token' => [
                'class' => 'OAuth2\GrantType\RefreshToken',
                'always_issue_new_refresh_token' => true
            ]
        ]
    ]
]

storageMap裡的user_credentials和user_claims都是指向 app\models\OauthUsers ,app\models\OauthUsers是對應專案裡使用者資料表的ActiveRecord,user_credentials是用來認證使用者身份,需在app\models\OauthUsers實作OAuth2\Storage\UserCredentialsInterface界面;user_claims是用來取得使用者資料,需在app\models\OauthUsers實作 OAuth2\OpenID\Storage\UserClaimsInterface界面。

grantTypes裡的 authorization_code,是取得code之後要交換access_token用的,原始的extension說明裡沒有提到,需加上才能正確運作。

       'authorization_code' => [
            'class' => 'OAuth2\GrantType\AuthorizationCode',
        ],

設定完成之後,這個套件已經提供資料表結構可供匯入,請在命令列執行

yii migrate --migrationPath=@vendor/hosannahighertech/yii2-oauth2-server/migrations

匯入的資料表有這些:

oauth_access_tokens - 儲存access token
oauth_authorization_codes 儲存authorize code	
oauth_clients - 儲存client
oauth_jwt - jwt認證資料
oauth_public_keys - 
oauth_refresh_tokens - 
oauth_scopes - 儲存 scope
oauth_users - 儲存使用者資料

在串接facebook登入或是google登入時,我們都需要先申請appid (或client_id) 和secret,同時要設定redirect_uri, 這個資料就存放在 oauth_clients,設定的界面我們需要自己完成,但測試時可以直接從資料庫修改資料即可。可以直接利用預設提供的testclient, testpass作為client id及client secret,也可以另外新增記錄,grant_types是認證的方式,你果你不確定這是什麼,新增記錄時最好讓它跟第一筆記錄裡的內容一樣,以免影響你實作。

oauth_users是使用者資料表,登入的使用者帳密就是放在這個資料表,在實際登入前,請先增加一筆你要登入使用的帳號和密碼。預設的主鍵是username,我自己再新增一個欄位叫id,改主鍵為id,如果沒 改,後面利用access token取得使用者資料的欄位就要改。

實作Oauth2 Server

接下來Service端的實作有三個項目要做,分別是認證、交換access_token,以及取得使用者資料,認證和交換access_token可以規劃在同一個controller實作,而取得使用者資料因為要利用取得的access_token來認證身份,會利用yii\filters\auth\CompositeAuth這個controller filter來認證身份,建議是另設controller來實作。

認證和交換access token的實作如下:

use Yii;
use yii\helpers\ArrayHelper;
use yii\helpers\VarDumper;
use yii\web\Controller;
use yii\web\Response;
use app\models\LoginForm;

class SiteController extends Controller
{
    public $enableCsrfValidation = false;

    /**
     * Login action.
     *
     * @return Response|string
     */
    public function actionLogin()
    {
        if (!Yii::$app->user->isGuest) {
            return $this->goHome();
        }
        $model = new LoginForm();
        if ($model->load(Yii::$app->request->post()) && $model->login()) {
            return $this->redirect(ArrayHelper::merge(['site/authorize'],Yii::$app->request->queryParams));   //登入完成後要重新導向authorize頁面,網址參數也一併帶過去。
        }

        $model->password = '';
        return $this->render('login', [
            'model' => $model,
        ]);
    }

    public function actionAuthorize()
    {
        if (Yii::$app->getUser()->getIsGuest())
            return $this->redirect(ArrayHelper::merge(['login'],Yii::$app->request->queryParams));  //未登入使用者需先登入,帶過來的網址參數也要一併帶過去。
        /** @var $module \filsh\yii2\oauth2server\Module */
        $module = Yii::$app->getModule('oauth2');
        /** @var \OAuth2\Response $response  */
        $response = $module->getServer()->handleAuthorizeRequest(null,null,!Yii::$app->getUser()->getIsGuest(), Yii::$app->getUser()->getId());
        $headers = $response->getHttpHeaders();
        return $this->redirect($headers['Location']);
    }

    public function actionToken()
    {
        /** @var $module \filsh\yii2\oauth2server\Module */
        $module = Yii::$app->getModule('oauth2');
        /** @var \OAuth2\Response $response  */
        $response = $module->getServer()->handleTokenRequest(null,null,!Yii::$app->getUser()->getIsGuest(), Yii::$app->getUser()->getId());
        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
        Yii::$app->response->content = $response->getResponseBody();
        return Yii::$app->response;
    }
}

接下來要做可取得使用者資料的Api Controller

use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;

class ApiController extends \yii\rest\Controller {

    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['authenticator'] = ArrayHelper::merge($behaviors['authenticator'], [
            'authMethods' => [
                ['class' => HttpBearerAuth::className()],
                ['class' => QueryParamAuth::className(), 'tokenParam' => 'access_token'],
            ]]);
        \Yii::debug('behaviors:'.VarDumper::export($behaviors));
        return $behaviors;
    }

    public function actionUserInfo(){
        $access_token = \Yii::$app->request->get("access_token");//如果用Yii預設的傳送token方式,是利用get的方式。
        $tokenModel = \app\models\OauthAccessTokens::find()->where(['access_token'=>$access_token])->one();
        $user = \app\models\OauthUsers::find()->where(['id'=>$tokenModel->user_id])->one();
        return $user->toArray();//實際上這裡可能要稍微處理一下,因為整個user裡面的資料可能有一些敏感不應輸出的內容,像是密碼。
    }

}

如果你的urlManager有照前述的config的設定

     'urlManager' => [
          'enablePrettyUrl' => true,
          'showScriptName' => false,
      ],

假設你的網址是http://myauth.server

認證的網址是: http://myauth.server/site/authorize

交換access token的網址是: http://myauth.server/site/token

取得使用者資料的網址是: http://myauth.server/api/user-info

如果沒有設定urlManager,那網址應該會變成

認證的網址是: http://myauth.server/index.php?r=site/authorize

交換access token的網址是: http://myauth.server/index.php?r=site/token

取得使用者資料的網址是: http://myauth.server/index.php?r=api/user-info

以上是稍後實作client認證時會用到的網址。

實作Oauth2 client

client的部份請另建yii2專案。

Yii2已經將大部份的工作都做了,實作時只要新增一個繼承 yii\authclient\OAuth2 的類別即可,例如:

use yii\authclient\OAuth2;

class MyAuthClient extends Oauth2
{
    public $authUrl = 'http://myauth.server/site/authorize';
    public $apiBaseUrl = 'http://myauth.server/api';
    public $tokenUrl = 'http://myauth.server/site/token';
    public $scope = 'openid'; //如果要利用套件自帶的api取得使用者資料,這裡請一定要填openid

    protected function initUserAttributes()
    {
        //$token = $this->getAccessToken();
        //$headers = ['Authorization' => 'Bearer '.$token->token];
        $userInfo = $this->api('user-info', 'POST', [], $headers);
        return $userInfo;
    }
}

另外client端的設定如下:

'components'=>[
      'authClientCollection' => [
            'class' => 'yii\authclient\Collection',
            'clients' => [
                'my-auth-client' => [
                    'class' => 'app\components\authclient\MyAuthClient',
                    'clientId' => 'testclient',
                    'clientSecret' => 'testpass',
                    'returnUrl' => 'http://myauth.client/site/auth?authclient=my-auth-client',
                ],
                // etc.
            ],
        ],
],

上述的 clientId, clientSecret和returnUrl請務必設在service端的 oauth_clients 資料表裡。

接下來在SiteController裡設定Auth Action

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'auth' => [
                'class' => 'yii\authclient\AuthAction',
                'successCallback' => [$this, 'authSuccess'],
            ],
        ];
    }

    public function authSuccess(\yii\authclient\OAuth2 $client){
        $userAttrs = $client->getUserAttributes();
        //todo: 實作登入細節
    }
}

最後就是將登入按鈕做出來,在登入頁的view裡呼叫下列敘述即可:

<?= yii\authclient\widgets\AuthChoice::widget([
      'baseAuthUrl' => ['site/auth']
]); ?>

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *