安装包
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;}