PHP 前后端token交互逻辑实现


前后端分离后,认证方式有多种,下面说一下选择使用token作为认证的解决方案。


正文

前后端分离后,认证方式有多种,下面说一下选择使用token作为认证的解决方案,使用场景是嵌入式交互。

项目立项(假设项目名是项目ws),开发框架选择Yii2,token处理包选择lcobucci/jwt。

交互方式是:

bbs前端点击 ws按钮 ,向 bbs后端 发起请求,bbs后端 带上认证信息和请求的客户信息向 ws项目 获取token,ws项目 验证请求方信息,然后生成令牌 token,再把 token返回给 bbs后端,bbs后端 再把 token返回给 bbs前端,以后 bbs前端 就与 ws项目直接发生交互,每次交互都带上 token。

下面正式展开。

首先在ws项目的composer.json包管理中引入lcobucci/jwt,并更新:

"require": {
    "php": ">=7.0.0",
    "yiisoft/yii2": "=2.0.8",
    "lcobucci/jwt": "^3.2"
},

现在的前后端交互一般用form表单传递数据,但最好选用json格式,这样文件内容格式会更丰富,我们需要修改ws的component的request组件,加入json自动解析类:

'request' => [
    // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
    'cookieValidationKey' => 'X5fkO11SJRLNpeWi_bdkrTM_dKsQgSA-',
    'parsers'    =>  [
        'application/json' => 'yii\web\JsonParser',
        'text/json' => 'yii\web\JsonParser',
    ],
],

共同入口控制器中准备post请求的参数:

protected $global_post = [];  // post提交参数

public function init()
{
    parent::init();
    // post参数获取
    $req = Yii::$app->request;
    if ( !$req->isPost ) {
        return [
            'code' => 2001,
            'msg' => '提交方式错误',
            'data' => [],
        ];
    }
    $this->global_post = $req->post();
    foreach ( $this->global_post as $key => &$value )
    {
        $value = htmlspecialchars( trim( $value ) );
    }
}

ws的params文件中写入配置参数:

'params' => [
    'jwt' => [
        'iss' => 'https://ws.ali.com',  // 签发者
        'exp' => 0,  // 过期时间戳
        'sub' => 0,  //面向的用户
        'sur' => 86400,  // 时长
        'src' => 'bbs',  // 来源
        'aud' => 'http://bbs.ali.com', // 接收方
        'iat' => '', //签发时间,
        "salt" => "abc123",  // 加盐字符串
    ],
    
    'srcAccountInfo' => [
        'bbs' => [
            'appId' =>  'bbs',
            'appSecret' => 'bbs2013',
            'token' => 'bbs20130128'
        ],
        'tb' => [
            'appId' =>  'tb',
            'appSecret' => 'tb2013',
            'token' => 'tb20130128'
        ],
    ],
],

ws中对token签发和验证的逻辑封装:

namespace common\service;

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;
use \Exception;
use Yii;

class LTokenService
{
    /**
     * 生成令牌
     * @param $from_type
     * @param $user_id
     * @return string
     */
    public static function createToken( $from_type, $user_id = 0 )
    {
        $jwt = Yii::$app->params['jwt'];
        $jwt['aud'] = Yii::$app->request->getUserIP();
        $jwt['jti'] = uniqid() . mt_rand(100000, 999999);
        $jwt['iat'] = time();
        $jwt['nbf'] = time();
        $jwt['exp'] = $jwt['iat'] + $jwt['sur'];
        $jwt['src'] = $from_type;
        $jwt['user_id'] = $user_id;

        $signer = new Sha256();
        $token = (new Builder())->setIssuer($jwt['iss'])  // 签发者
        ->setAudience( $jwt['aud'] )  //  接受者
        ->setId( $jwt['jti'], true )  // 唯一标识
        ->setIssuedAt( $jwt['iat'] )  // 签发时间
        ->setNotBefore( $jwt['nbf'])  // 过去时间不可用
        ->setExpiration( $jwt['exp'] )  // 截至时间
        ->set('user_id', $jwt['user_id'])  // 员工id
        ->set('src', $jwt['src'])  // 来源
        ->set('jti', $jwt['jti'])  // 唯一标识
        ->sign($signer, $jwt["salt"])  // 加盐字符串,如 'ali2013'
        ->getToken();

//        $token->getHeaders(); // Retrieves the token headers
//        $token->getClaims(); // Retrieves the token claims

        return (string)$token;
    }

    /**
     * 检测Token是否过期与篡改
     * @param null $token
     * @return array
     * @throws Exception
     */
    public static function validateToken( $token = null )
    {
        $token = (new Parser())->parse($token); // Parses from a string
        
        $jwt = Yii::$app->params['jwt'];
        
        $signer = new Sha256();
        if (!$token->verify($signer, $jwt["salt"])) {
            throw new Exception('签名不正确');
        }

        $aud = Yii::$app->request->getUserIP();

        $validationData = new ValidationData();
        $validationData->setIssuer( $jwt['iss'] );
        $validationData->setAudience( $aud );  // 令牌异地检查
        $validationData->setId( $token->getClaim('jti') );  // 唯一标识

        if ( $token->validate($validationData) ) {
            return [
                'src' => $token->getClaim('src'),
                'user_id' => $token->getClaim('user_id'),
            ];
        } else {
            throw new Exception('令牌异地或过时');
        }
    }
}

现在bbs后端请求ws获取token:

请求地址:https://ws.ali.com/user/get-jwt-token

post方式,json格式请求:

{
    "src":"bbs",
    "appId":"bbs",
    "appSecret":"bbs2013",
    "userId":867,
}

ws的user/get-jwt-token接受逻辑:

use common\service\LTokenService;

public function actionGetJwtToken()
{
    $from_type = !empty($this->global_post['src']) ? $this->global_post['src'] : '';
    $appId = !empty($this->global_post['appId']) ? $this->global_post['appId'] : '';
    $appSecret = !empty($this->global_post['appSecret']) ? $this->global_post['appSecret'] : '';
    
    if ( empty($from_type) || empty($appId) || empty($appSecret) ) {
        return [
            'code' => 2010,
            'msg' => '提交参数不能为空',
            'data' => ,
        ];
    }
    
    $params_src = Yii::$app->params['srcAccountInfo'];
    if ( !isset( $params_src[ $from_type ] ) ) {
         return [
            'code' => 2015,
            'msg' => '来源项目未授权',
            'data' => ,
        ];
    }
    
    $systemData = $params_src[ $from_type ];
    if ( ($appId != $systemData['appId']) || ($appSecret != $systemData['appSecret']) ) {
        return [
            'code' => 2020,
            'msg' => '来源项目接入参数错误',
            'data' => ,
        ];
    }
    
    // 生成令牌
    $token = LTokenService::createToken( $from_type, user_id );
    
    return [
        'code' => 200,
        'msg' => 'ok',
        'data' => [
            'token' => $token,
        ],
    ];
}

bbs拿到token,以后请求都在head头信息中带上jwt-token。

看一下ws共同入口处的过滤操作:

public function beforeAction( $action )
{
    if ( $action->actionMethod != 'actionGetJwtToken' ) {
        /*****   检查令牌  ******/
        $jwtToken = Yii::$app->request->headers->get('jwt-token');
        if ( isset($jwtToken) ) {
            try {
                $res = LTokenService::validateToken( $jwtToken );
                
                $this->global_src = $res['src'];
                $this->global_user_id = $res['user_id'];
            } catch ( \Exception $e ) {
                $result = [
                    'code' => 401,
                    'msg' => '授权失败,请刷新页面后重新操作',
                    'data' => [],
                ];
                
                Yii::$app->response->format = Response::FORMAT_JSON;
                Yii::$app->response->data = $result;
                
                return false;
            }
        } else {
            return [
                'code' => 401,
                'msg' => '用户未授权',
                'data' => [],
            ];
        }
    }
    
    return parent::beforeAction($action);
}

上面我们自己在控制器中写了beforeAction()方法来进行用户身份判断,然后保存为全局变量进行后续操作。 其实我们从上面代码可以看到,如果我们把用户信息直接赋值给Yii::$app->user组件,那以后我们操作起来会很方便。

行为自动认证

我们可以把用户登录那一段代码参考过来,实现用户通过token自动登录功能。

借鉴Yii2的behavior行为拓展控制器功能,在Controller控制器中完善behaviors()方法:

use backend\components\filters\AuthorizationControl;
use yii\helpers\ArrayHelper;

...

public function behaviors()
{
    return ArrayHelper::merge([
        'AuthorizationControl' => AuthorizationControl::className(),
    ], parent::behaviors());
}

backend\components\filters\AuthorizationControl文件内容:

<?php
namespace backend\components\filters;

use Yii;
use yii\base\Behavior;
use yii\web\IdentityInterface;
use yii\web\Request;
use yii\web\Response;
use yii\web\UnauthorizedHttpException;
use yii\web\User;

class AuthorizationControl extends yii\base\Behavior
{
    public function beforeAction($action)
    {
        $identity = $this->authenticate(
            Yii::$app->getUser(),
            Yii::$app->getRequest(),
            Yii::$app->getResponse()
        );

        if ($identity === null) {
            $this->handleFailure($response);
        }
        
        return parent::beforeAction($action);
    }
    
    /**
     * Authenticates the current user.
     * @param User $user
     * @param Request $request
     * @param Response $response
     * @return IdentityInterface the authenticated user identity. If authentication information is not provided, null will be returned.
     * @throws UnauthorizedHttpException if authentication information is provided but is invalid.
     */
    public function authenticate($user, $request, $response)
    {
        $authHeader = $request->getHeaders()->get('Authorization');  // 获取http认证头,用户端会在我们返回的认证信息前面加 令牌 Bearer
        if ($authHeader !== null && preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) {
            $identity = $user->loginByAccessToken($matches[1], get_class($this));
            if ($identity === null) {
                $this->handleFailure($response);
            }
        }
        
        return null;   
    }
    
    /**
     * Handles authentication failure.
     * The implementation should normally throw UnauthorizedHttpException to indicate authentication failure.
     * @param Response $response
     * @throws UnauthorizedHttpException
     */
    public function handleFailure($response)
    {
        throw new UnauthorizedHttpException('You are requesting with an invalid credential.');
    }
}

上面看到有User组件出现,我们在项目配置文件中配置的User组件为:

"components" => [
    "user" => [
        "class" => yii\web\User,
        "identityClass" => "backend\models\User",
    ],
],

在User组件yii\web\User类中有:

/**
 * Logs in a user by the given access token.
 * This method will first authenticate the user by calling [[IdentityInterface::findIdentityByAccessToken()]]
 * with the provided access token. If successful, it will call [[login()]] to log in the authenticated user.
 * If authentication fails or [[login()]] is unsuccessful, it will return null.
 * @param string $token the access token
 * @param mixed $type the type of the token. The value of this parameter depends on the implementation.
 * For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`.
 * @return IdentityInterface|null the identity associated with the given access token. Null is returned if
 * the access token is invalid or [[login()]] is unsuccessful.
 */
public function loginByAccessToken($token, $type = null)
{
    /* @var $class IdentityInterface */
    $class = $this->identityClass;
    $identity = $class::findIdentityByAccessToken($token, $type);
    if ($identity && $this->login($identity)) {
        return $identity;
    } else {
        return null;
    }
}

/**
 * Logs in a user.
 *
 * After logging in a user, you may obtain the user's identity information from the [[identity]] property.
 * If [[enableSession]] is true, you may even get the identity information in the next requests without
 * calling this method again.
 *
 * The login status is maintained according to the `$duration` parameter:
 *
 * - `$duration == 0`: the identity information will be stored in session and will be available
 *   via [[identity]] as long as the session remains active.
 * - `$duration > 0`: the identity information will be stored in session. If [[enableAutoLogin]] is true,
 *   it will also be stored in a cookie which will expire in `$duration` seconds. As long as
 *   the cookie remains valid or the session is active, you may obtain the user identity information
 *   via [[identity]].
 *
 * Note that if [[enableSession]] is false, the `$duration` parameter will be ignored as it is meaningless
 * in this case.
 *
 * @param IdentityInterface $identity the user identity (which should already be authenticated)
 * @param integer $duration number of seconds that the user can remain in logged-in status.
 * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed.
 * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported.
 * Note that if [[enableSession]] is false, this parameter will be ignored.
 * @return boolean whether the user is logged in
 */
public function login(IdentityInterface $identity, $duration = 0)
{
    if ($this->beforeLogin($identity, false, $duration)) {
        $this->switchIdentity($identity, $duration);
        $id = $identity->getId();
        $ip = Yii::$app->getRequest()->getUserIP();
        if ($this->enableSession) {
            $log = "User '$id' logged in from $ip with duration $duration.";
        } else {
            $log = "User '$id' logged in from $ip. Session not enabled.";
        }
        Yii::info($log, __METHOD__);
        $this->afterLogin($identity, false, $duration);
    }

    return !$this->getIsGuest();
}

user组件中identityClass,是自己创建的model类(Yii2会自动生成),我们可以修改下:

namespace backend\models;

use common\service\LTokenService;
use Yii;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;

class User extends ActiveRecord implements IdentityInterface
{
    ...
    
    /**
     * {@inheritdoc}
     */
    public static function findIdentity($id)
    {
        return static::findOne(['id' => $id]);
    }
    
    public static function findIdentityByAccessToken($token, $type = null)
    {
        $result = LTokenService::validateToken($token);
        
        if (!empty($result["user_id"])) {
            $user = self::findIdentity($result["user_id"]);
            if (empty($user)) {
                throw new \Exception("未找到该用户,请确认该用户是否合法");
            }
        } else {
            $user = new self();
            $user->id = "";
        }
        
        $user->src = !empty($result["src"]) ? $result["src"] : "";
        
        return $user;
    }
}






参考资料

github jwt https://github.com/lcobucci/jwt

Documentation https://lcobucci-jwt.readthedocs.io/en/latest/

lcobucci/jwt的安装和使用 https://www.cnblogs.com/jurij/p/jwt.html

lcobucci/jwt —— 一个轻松生成jwt token的插件 http://www.koukousky.com/back/2483.html

深入理解Yii2.0 行为 https://ibaiyang.github.io/blog/yii2/2019/06/21/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Yii2.0-%E8%A1%8C%E4%B8%BA.html

Yii 2.0 权威指南 》 安全(Security): 授权(Authorization) https://www.yiichina.com/doc/guide/2.0/security-authorization

https://laravel-china.org/articles/9122/composer-uses-jwt-to-generate-token-instances

http://www.ptbird.cn/jquery-test-jwt.html

http://www.cnblogs.com/zjutzz/p/5790180.html

https://blog.csdn.net/zy994914376/article/details/79579101


返回