Lumen7.x 使用笔记(四)JWT-AUTH认证

作者: 太阳上的雨天 分类: Lumen7.x使用教程,PHP 发布时间: 2020-09-30 15:42

安装包

composer require tymon/jwt-auth

Bootstrap file changes.

Add the following snippet to the bootstrap/app.php file under the providers section as follows:

// Uncomment this line
$app->register(App\Providers\AuthServiceProvider::class);

// Add this line
$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);

Then uncomment the auth middleware in the same file:

$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
]);

Generate secret key

I have included a helper command to generate a key for you:

php artisan jwt:secret

This will update your .env file with something like JWT_SECRET=foobar

It is the key that will be used to sign your tokens. How that happens exactly will depend on the algorithm that you choose to use.

config/auth.php file changes.

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'api'),
        'passwords' => 'users',
    ],

    'guards' => [
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],

    'providers' => [
        //
        'users' => [
            'driver' => 'eloquent',
            'model'  => App\Model\UserModel::class
        ]
    ],

app/Http/Middleware/Authenticate.php

<?php
namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\JWTAuth;
use Illuminate\Http\Request;
use App\Enums\WopCode;

class Authenticate
{

    /**
     * @var JWTAuth $jwtAuth
     */
    protected $jwtAuth;

    /**
     * Create a new middleware instance.
     *
     * @param JWTAuth $jwtAuth
     */
    public function __construct(JWTAuth $jwtAuth)
    {
        $this->jwtAuth = $jwtAuth;
    }

    /**
     * Handle an incoming request.
     *
     * @param Request $request
     * @param Closure $next
     * @return mixed
     * @throws JWTException
     */
    public function handle(Request $request, Closure $next)
    {
        if (!$this->jwtAuth->parseToken()->check()) {
            $data = [
                'code' => WopCode::VERIFY_FIELD,
                'msg'  => '没有权限',
                'nowTime' => time(),
            ];
            return response($data);
        }
        return $next($request);
    }
}

Update your User model

Firstly you need to implement the Tymon\JWTAuth\Contracts\JWTSubject contract on your User model, which requires that you implement the 2 methods getJWTIdentifier() and getJWTCustomClaims().

The example below should give you an idea of how this could look. Obviously you should make any changes, as necessary, to suit your own needs.

<?php
namespace App\Model;

use Illuminate\Auth\Authenticatable;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Database\Eloquent\Model;
use Tymon\JWTAuth\Contracts\JWTSubject;

class UserModel extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
    use Authenticatable, Authorizable;

    /**
     * created_at
     */
    const CREATED_AT = 'createdAt';

    /**
     * update_at
     */
    const UPDATED_AT = 'updatedAt';

    /**
     * @var string
     */
    protected $table = 'user';

    protected $fillable = [
        'userInfoId',
        'phone',
        'password',
        'name',
        'ip',
        'email',
        'address',
        'idCard',
        'idCardFront',
        'idCardFront',
        'idCardAfter',
        'createdAt',
        'updatedAt'
    ];

    /**
     * @var string[]
     */
    protected $hidden = [
        'password'
    ];

    public function getJWTIdentifier()
    {
        // TODO: Implement getJWTIdentifier() method.
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        // TODO: Implement getJWTCustomClaims() method.
        return [];
    }

}

Add some basic authentication routes

First let’s add some routes in routes/open.php as follows:

<?php
use Illuminate\Support\Facades\Route;

Route::group(['prefix'=>'v1', 'namespace'=>'Open'], function () {
    Route::post('register', 'AuthController@register');
    Route::post('login', 'AuthController@login');
});

Route::group(['prefix'=>'v1', 'middleware'=>'auth:api', 'namespace'=>'Open'], function () {
    Route::get('get-user-token', 'AuthController@getUserToken');
    Route::get('refresh', 'AuthController@refresh');
    Route::get('logout', 'AuthController@logout');
});

Create the AuthController

app/Http/Controller/Open/AuthController.php

<?php
namespace App\Http\Controllers\Open;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTAuth;
use App\Services\UserService;  // UserService 用户服务逻辑处理层

class AuthController extends Controller
{
    /**
     * @var int
     */
    protected $currentDateTime;

    /**
     * @var JWTAuth
     */
    protected $jwt;

    /**
     * @var UserService
     */
    protected $userService;

    /**
     * AuthController constructor.
     * @param JWTAuth $jwt
     * @param UserService $userService
     */
    public function __construct(JWTAuth $jwt, UserService $userService)
    {
        $this->jwt = $jwt;
        $this->currentDateTime = time();
        $this->userService = $userService;
    }

    /**
     * Notes: 登录
     * User: clj
     * Date: 2020/9/18
     * Time: 6:35 下午
     * @param Request $request
     * @return array
     */
    public function login(Request $request)
    {
        $validator = $this->getValidationFactory()->make($request->all(), [
            'phone' => 'required|regex:/^1[3456789]\d{9}$/',
            'password' => 'required',
        ]);
        if ($validator->fails()) {
            return $this->customError($validator->errors()->all());
        }
        return $this->userService->loginService($request->all());
    }

    /**
     * Notes: 退出
     * User: clj
     * Date: 2020/9/18
     * Time: 6:35 下午
     * @return array
     */
    public function logout()
    {
        return $this->userService->logoutService();
    }

    /**
     * Notes: 刷新Token
     * User: clj
     * Date: 2020/9/18
     * Time: 6:35 下午
     * @return array
     */
    public function refresh()
    {
        return true
        // return $this->userService->refreshService();
    }

    /**
     * Notes: 获取Token
     * User: clj
     * Date: 2020/9/18
     * Time: 6:35 下午
     * @return array
     */
    public function getUserToken()
    {
        return $this->userService->getUserTokenService();
    }

    /**
     * Notes: 注册
     * User: clj
     * Date: 2020/9/18
     * Time: 6:35 下午
     * @param Request $request
     * @return array
     */
    public function register(Request $request)
    {
        $validator = $this->getValidationFactory()->make($request->all(), [
            'phone' => 'required|regex:/^1[3456789]\d{9}$/',
            'password' => 'required',
        ]);
        if ($validator->fails()) {
            return $this->customError($validator->errors()->all());
        }
        return $this->userService->registerService($request->all());
    }

}

Add UserService

app/Services/UserService.php

<?php
namespace App\Services\Developer;

use Exception;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use App\Tools\Automatic;
use Illuminate\Support\Facades\DB;
use App\Repository\DeveloperRepository;
use App\Repository\DeveloperAppRepository;
use App\Enums\WopMessage;
use Tymon\JWTAuth\JWTAuth;
use Webpatser\Uuid\Uuid;

class AuthService
{
    /**
     * UNDESERVING
     */
    private const UNDESERVING = 4;
    /**
     * DAMNATION
     */
    private const DAMNATION = 1;
    /**
     * APPLICATION
     */
    private const APPLICATION = 2;

    /**
     * 当前时间戳
     * currentDateTime
     */
    protected const currentDateTime = null;

    /**
     * @var JWTAuth $jwtAuth
     */
    protected $jwtAuth;

    /**
     * @var DeveloperRepository $developerRepository
     */
    protected $developerRepository;

    /**
     * @var DeveloperAppRepository $developerApplicationRepository
     */
    protected $developerApplicationRepository;

    /***
     * AuthService constructor.
     * @param JWTAuth $jwtAuth
     * @param DeveloperRepository $developerRepository
     * @param DeveloperAppRepository $developerApplicationRepository
     */
    public function __construct(
        JWTAuth $jwtAuth,
        DeveloperRepository $developerRepository,
        DeveloperAppRepository $developerApplicationRepository
    )
    {
        $this->jwtAuth = $jwtAuth;
        $this->developerRepository = $developerRepository;
        $this->developerApplicationRepository = $developerApplicationRepository;
    }

    /**
     * Notes: 登录
     * @param array $params
     * @return array|false
     */
    public function loginService(array $params)
    {
        $user = $this->developerRepository->findBy('phone',$params['phone']);
        if (empty($user)) {
            return WopMessage::ACCOUNT_NOT_EXISTS;
        }
        if ($user->status == 2) {
            return WopMessage::ACCOUNT_FAIL;
        }
        if (!Hash::check($params['password'], $user->password)) {
            return WopMessage::PWD_ERR;
        }
        $oldToken = $user->jwtToken;
        if (!($token = Auth::login($user))) {
            return WopMessage::LOG_FIELD;
        }
        if ($user->expireTime + env('JWT_TTL') * 60 > time()) {  
          if (!$this->jwtAuth->check()) {  
              $this->jwtAuth->setToken($oldToken)->invalidate();  
          }  
        }
        $this->developerRepository->update(['jwtToken' => $token,'expireTime'=>time()], $user->userInfoId, 'userInfoId');
        return ['jwtToken' => $token];
    }

    /**
     * Notes: 退出
     * @return boolean
     */
    public function logoutService()
    {
        Auth::logout();
        return true;
    }

    // public function refreshService()
    // {
    //     // $user = $this->developerRepository->findBy('phone', Auth::user()->phone);
    //     // if (!$token = Auth::refresh(true, true)) {
    //     //     return WopMessage::GENERATE_ERR;
    //     // }
    //     // $this->developerRepository->update(['jwtToken' => $token], $user->userInfoId, 'userInfoId');
    //     // // return ['jwtToken' => $token];
    // }

    /**
     * Notes: 注册
     * @param array $userInfo
     * @return array|boolean
     * @throws Exception
     */
    public function registerService(array $userInfo)
    {
        $userInfo['password'] = Hash::make($userInfo['password']);
        do {
            $userInfo['userInfoId'] = Automatic::automaticGenerated(self::UNDESERVING);
            $userInfoId = $this->developerRepository->findBy('userInfoId', $userInfo['userInfoId']);
        } while ($userInfoId);
        $userInfo['name'] = Automatic::automaticGenerated(self::DAMNATION);
        $userInfo['ip'] = getIp();
        do {
            $application['appId'] = $userInfo['userInfoId'] . Automatic::automaticGenerated(self::APPLICATION);
            $appId = $this->developerApplicationRepository->findBy('appId', $application['appId']);
        } while ($appId);
        $application['appSecret'] = Uuid::generate()->string;
        $application['userInfoId'] = $userInfo['userInfoId'];
        DB::beginTransaction();
        try {
            $user = $this->developerRepository->create($userInfo);
            $this->developerApplicationRepository->create($application);
            DB::commit();
        } catch (Exception $e) {
            DB::rollBack();
            return WopMessage::REG_FIELD;
        }
        if (!$token = Auth::login($user)) {
            return WopMessage::REG_FIELD;
        }
        $this->developerRepository->update(['jwtToken'=>$token, 'expireTime'=>time()], $user->userInfoId, 'userInfoId');
        return ['jwtToken' => $token];
    }
}

You should now be able to POST to the login endpoint (e.g. http://example.dev/v1/login) with some valid credentials and see a response like:

{
    "code": 2000,
    "msg": "success",
    "nowTime": 1600484396,
    "data": {
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC93cHQub3Blbi5jb21cL3YxXC9sb2dpbiIsImlhdCI6MTYwMDQ4NDM5NiwiZXhwIjoxNjAwNDg3OTk2LCJuYmYiOjE2MDA0ODQzOTYsImp0aSI6IjVCNEVKYTBWakpOQ1JCUlciLCJzdWIiOjExLCJwcnYiOiI5ZjVlNDU0YzIwYWJlN2RlYzFkYjA4MzI4Yzc4ZDg2YjVjZGRkYWVjIn0.80wwCcqVkgrsE8jajgQzRXlJaUxG5iddxZrlGgqtfiM"
    }
}

Authorization header

Authorization: Bearer eyJhbGciOiJIUzI1NiI...

问题一 优化 (JWTAuth黑名单)

1. 点击两次登录,第一次生成的token还能使用,正常来讲应该失效
2. 同一时间只允许登录唯一一台设备。例如设备 A 中用户如果已经登录,那么使用设备 B 登录同一账户,设备 A 就无法继续使用了。
解决办法:

我们可以给用户表新增一个字段,或者单独使用一张表,总之是需要先将用户的 Token 存下来,那么下次用户再次登录时,将旧 Token 加入黑名单
具体实现:
user表(用户)新增一个jwtToken字段

CREATE TABLE `w_user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主索引',
  `userInfoId` char(10) NOT NULL DEFAULT '' COMMENT '开发者唯一标识',
  `phone` char(11) NOT NULL DEFAULT '' COMMENT '开发者手机号',
  `password` char(60) NOT NULL DEFAULT '' COMMENT '开发者账号密码',
  `name` varchar(20) DEFAULT '' COMMENT '开发者用户名',
  `jwtToken` varchar(500) DEFAULT '' COMMENT 'oldToken',
  `expireTime` int(11) DEFAULT NULL COMMENT 'Token更新时间',
  `email` varchar(50) NOT NULL DEFAULT '' COMMENT '开发者邮箱',
  `address` varchar(50) DEFAULT NULL COMMENT '地址',
  `ip` varchar(30) NOT NULL DEFAULT '' COMMENT 'ip',
  `idCard` varchar(20) DEFAULT NULL COMMENT '身份证号',
  `idCardFront` varchar(100) DEFAULT NULL COMMENT '身份证正面',
  `idCardAfter` varchar(100) DEFAULT NULL COMMENT '身份证反面',
  `type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '类型 (1:个人 2:商家)',
  `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态 (1: 启用  2:禁用)',
  `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `userInfoId` (`userInfoId`),
  UNIQUE KEY `phone` (`phone`),
  UNIQUE KEY `idCard` (`idCard`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

登录方法文件引入JWTAuth

use Tymon\JWTAuth\JWTAuth;

/**
* @var JWTAuth $jwtAuth
*/
protected $jwtAuth;

public function __construct(
        JWTAuth $jwtAuth,
        DeveloperRepository $developerRepository,
        DeveloperAppRepository $developerApplicationRepository
    )
    {
        $this->jwtAuth = $jwtAuth;
        $this->developerRepository = $developerRepository;
        $this->developerApplicationRepository = $developerApplicationRepository;
    }

/**
     * Notes: 登录
     * @param array $params
     * @return array|false
     */
    public function loginService(array $params)
    {
        $user = $this->developerRepository->findBy('phone',$params['phone']);
        $oldToken = $user->jwtToken;  // 将oldToken先存入一个变量
        if (empty($user)) {
            return WopMessage::ACCOUNT_NOT_EXISTS;
        }
        if ($user->status == 2) {
            return WopMessage::ACCOUNT_FAIL;
        }
        if (!Hash::check($params['password'], $user->password)) {
            return WopMessage::PWD_ERR;
        }
        if (!($token = Auth::login($user))) {
            return WopMessage::LOG_FIELD;
        }
        if (false == $this->jwtAuth->check()) {  
          $this->jwtAuth->setToken($oldToken)->invalidate();  
        }  
        $this->developerRepository->update(['jwtToken' =>        $token,'expireTime'=>time()], $user->userInfoId, 'userInfoId');  
        return ['jwtToken' => $token];
    }

问题二 刷新使 oldToken失效

解决办法一:
/**
     * Notes: 刷新
     * @return array|boolean
     */
    public function refreshService()
    {
        $user = $this->developerRepository->findBy('phone', Auth::user()->phone); // 获取用户信息(手机号)
      // token生成成功,将oldToken加入黑名单
        if (!$token = Auth::refresh(true, true)) {
            return WopMessage::GENERATE_ERR;
        }
                // 把新的Token写入数据库
        $this->developerRepository->update(['jwtToken' => $token], $user->userInfoId, 'userInfoId'); 
        return ['jwtToken' => $token];
    }

推荐第二种: 中间件处理

新建RefreshToken.php
<?php
namespace App\Http\Middleware;

use App\Enums\WopCode;
use App\Enums\WopMessage;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use App\Repository\DeveloperRepository;
use Tymon\JWTAuth\JWTAuth;

class RefreshToken extends BaseMiddleware
{
    protected $developerRepository;

    public function __construct(JWTAuth $auth, DeveloperRepository $developerRepository)
    {
        parent::__construct($auth);
        $this->developerRepository = $developerRepository;
    }

    /**
     * Notes:
     * Date: 2020/9/24
     * @param Request $request
     * @param Closure $next
     * @return string
     */
    public function handle(Request $request, Closure $next)
    {
        if (empty($request->header('Authorization'))) {
            $response = [
                'code' => WopCode::VERIFY_FIELD,
                'data' => WopMessage::ILLEGAL_REQUEST,
            ];
            return response($response);
        }
        $token = Auth::refresh();
        if (!$token) {
            $response = [
                'code'  => WopCode::GENERATE_ERR,
                'data'  => WopMessage::GENERATE_ERR
            ];
            return response($response);
        }
        $id = $this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub'];
        if (!Auth::guard('api')->onceUsingId($id)) {
            $response = [
                'code'    => WopCode::RESET_LOGIN,
                'data'    => WopMessage::RESET_LOGIN
            ];
            return response($response);
        }
        $this->developerRepository->update(['jwtToken' => $token], Auth::user()->userInfoId, 'userInfoId');
        return $this->setAuthenticationHeader($next($request), $token);
    }
}

bootstarp/app.php 注册到路由中间件
$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
    'refresh' => App\Http\Middleware\RefreshToken::class  // 刷新中间件
]);

// 获取开发者token、退出群组
Route::group(['prefix'=>'v1', 'middleware'=>'auth:api', 'namespace'=>'Developer'], function () {
    Route::get('get-user-token', 'AuthController@getUserToken');
    // Route::get('refresh', 'AuthController@refresh');  // 注释
    Route::get('logout', 'AuthController@logout');
});

// 刷新
Route::group(['prefix'=>'v1', 'middleware'=>['refresh'], 'namespace'=>'Developer'], function () {
    Route::get('refresh', 'AuthController@refresh');
});

控制器AuthController.php
 /**
     * @return bool
     */
    public function refresh()
    {
        return true;
    }

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注