正文
前后端分离后,认证方式有多种,下面说一下选择使用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