1./open/auth/gettoken
入参
{
appkey:"xxx",
appsecret:"xxxx"
}
返回
{
"access_token":xxx // 2小时过期
"refresh_token":xxx // 14天过期
"expire": // access_token 过期时间
}
2.刷新token接口:access_token过期就拿着refresh_token去获取新的access_token,这样就避免带着appsecret等敏感信息,长期token获取短期token
3.其他请求资源接口:通过access_token去请求资源
基于jwt:
jwt的playload不要存储敏感信息,其内容主要基于base64处理,放置的内容越多,长度也越长,同时是可以解码的jwt.io/
在 JWT 的实践中,引入 Refresh Token,将会话管理流程改进如下。
- 客户端使用用户名密码进行认证
- 服务端生成有效时间较短的 Access Token(例如 10 分钟),和有效时间较长的 Refresh Token(例如 7 天)
- 客户端访问需要认证的接口时,携带 Access Token
- 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
- 如果携带 Access Token 访问需要认证的接口时鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
- 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token
- 客户端使用新的 Access Token 访问需要认证的接口
将生成的 Refresh Token 以及过期时间存储在服务端的数据库中,由于 Refresh Token 不会在客户端请求业务接口时验证,只有在申请新的 Access Token 时才会验证,所以将 Refresh Token 存储在数据库中,不会对业务接口的响应时间造成影响,也不需要像 Session 一样一直保持在内存中以应对大量的请求。
php版本简单实现没有刷新token
namespace App\Http\Controllers\Open\Auth;
use App\Http\Controllers\Controller;
use App\Http\Response\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Redis;
use Tymon\JWTAuth\Facades\JWTAuth;
use App\Http\Services\Clients\ClientsService;
use App\Enums\SystemErrorEnum;
use Illuminate\Support\Str;
use App\Constants\RedisKey;
use Illuminate\Support\Facades\Log;
class AuthController extends Controller
{
public function __construct(
private readonly ClientsService $clientsService
) {}
/**
* 获取 Token
*
* @param Request $request
* @return JsonResponse
*/
public function getToken(Request $request): JsonResponse
{
// 刷新阈值:过期前 2 天内刷新 Token
$refreshThreshold = 172800; // 2 天
$effectiveTime = 1296000; // 15 天有效期
$request->validate([
'appkey' => 'required|string',
'appsecret' => 'required|string',
]);
$appkey = $request->appkey;
// 验证客户端凭证
$clientInfo = $this->clientsService->getClientAppSecret($appkey);
if (!$clientInfo || !hash_equals($clientInfo->appsecret, $request->appsecret)) {
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_ERR->value, SystemErrorEnum::AUTH_TOKEN_ERR->getMessage());
}
$now = time();
$oldToken = Redis::get(RedisKey::CLIENT_TOKEN_APPKEY . $appkey);
$shouldRefresh = false;
$accessToken = $oldToken; // 默认返回旧 Token
$expireTime = null;
// 检查是否需要刷新 Token
if ($oldToken) {
try {
$payload = JWTAuth::setToken($oldToken)->getPayload();
$remainingTime = $payload['exp'] - $now;
if ($remainingTime <= $refreshThreshold) {
$shouldRefresh = true;
} else {
$expireTime = $payload['exp']; // 旧 Token 的过期时间
}
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
// Token 已过期,强制生成新 Token
$oldToken = null;
$shouldRefresh = true;
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
// Token 无效
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_INVALID->value, SystemErrorEnum::AUTH_TOKEN_INVALID->getMessage());
} catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
// 其他 JWT 异常
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_EXPIRED->value, SystemErrorEnum::AUTH_TOKEN_EXPIRED->getMessage());
}
} else {
// 如果没有旧 Token,生成新 Token
$shouldRefresh = true;
}
// 生成新 Token
if ($shouldRefresh) {
$expireTime = $now + $effectiveTime; // 15 天有效期
$newPayload = [
'client_no' => $clientInfo->client_no,
'ak' => $appkey,
'iat' => $now,
'exp' => $expireTime,
'jti' => Str::uuid(), // 唯一标识
];
$accessToken = JWTAuth::customClaims($newPayload)->fromUser($clientInfo);
// 原子操作更新 Redis
Redis::pipeline(function ($pipe) use ($appkey, $accessToken, $oldToken, $expireTime, $now, $effectiveTime) {
$pipe->setex(RedisKey::CLIENT_TOKEN_APPKEY . $appkey, $effectiveTime, $accessToken);
if ($oldToken) {
$remainingTime = $expireTime - $now;
$pipe->setex(RedisKey::CLIENT_TOKEN_BLACKLIST . $oldToken, $remainingTime, 1); // 将旧 Token 加入黑名单
}
});
}
return ApiResponse::success(
data: [
'access_token' => $accessToken,
'expire' => $expireTime,
]
);
}
/**
* 使 Token 失效
*
* @param Request $request
* @return JsonResponse
*/
public function invalidateToken(Request $request)
{
try {
// 获取当前 Token
$token = $request->header('Authorization');
if (!$token) {
return ApiResponse::fail(SystemErrorEnum::PARAMS_ERROR->value, SystemErrorEnum::PARAMS_ERROR->getMessage());
}
// 验证 Token 并获取 Payload
$payload = JWTAuth::setToken($token)->getPayload();
$appkey = $payload['ak'] ?? null;
if (!$appkey) {
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_INVALID->value, SystemErrorEnum::AUTH_TOKEN_INVALID->getMessage());
}
// 将 Token 加入黑名单
$remainingTime = $payload['exp'] - time(); // 计算剩余有效期
if ($remainingTime > 0) {
Redis::setex(RedisKey::CLIENT_TOKEN_BLACKLIST . $token, $remainingTime, 1);
}
// 删除 Redis 中的 Token
Redis::del(RedisKey::CLIENT_TOKEN_APPKEY . $appkey);
return ApiResponse::success();
} catch (\Exception $e) {
Log::error('invalidateToken error', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_ERR->value, SystemErrorEnum::AUTH_TOKEN_ERR->getMessage());
}
}
}
middleware 验证token
namespace App\Http\Middleware\OpenPlatform;
use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Illuminate\Support\Facades\Redis;
use App\Enums\SystemErrorEnum;
use App\Http\Response\ApiResponse;
use App\Constants\RedisKey;
use Illuminate\Support\Facades\Log;
class VerifyOpenToken
{
public function handle(Request $request, Closure $next)
{
$token = $request->header('Authorization');
if (!$token) {
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_ERR->value, SystemErrorEnum::AUTH_TOKEN_ERR->getMessage());
}
try {
// 验证 Token
$payload = JWTAuth::setToken($token)->getPayload();
$appkey = $payload['ak'];
$clientNo = $payload['client_no'];
// 检查 Redis 中是否存在该 Token
$storedToken = Redis::get(RedisKey::CLIENT_TOKEN_APPKEY . $appkey);
if (!$storedToken || $storedToken !== $token) {
Log::info('token_no_match', [
'appkey' => $appkey,
'client_no' => $clientNo,
'token' => $token,
'storedToken' => $storedToken
]);
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_ERR->value, SystemErrorEnum::AUTH_TOKEN_ERR->getMessage());
}
// 检查黑名单
if (Redis::exists(RedisKey::CLIENT_TOKEN_BLACKLIST . $token)) {
Log::info('token_blacklist', [
'token' => $token,
'client_no' => $clientNo
]);
// return response()->json(['message' => 'Token is blacklisted'], 401);
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_ERR->value, SystemErrorEnum::AUTH_TOKEN_ERR->getMessage());
}
// 将 client_no 注入到请求中,方便后续使用
$request->headers->add([
'client_no' => $clientNo
]);
return $next($request);
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_EXPIRED->value, SystemErrorEnum::AUTH_TOKEN_EXPIRED->getMessage());
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
//return response()->json(['message' => 'Token is invalid'], 401);
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_EXPIRED->value, SystemErrorEnum::AUTH_TOKEN_EXPIRED->getMessage());
} catch (JWTException $e) {
//return response()->json(['message' => 'Token error'], 401);
return ApiResponse::fail(SystemErrorEnum::AUTH_TOKEN_ERR->value, SystemErrorEnum::AUTH_ERROR->getMessage());
}
}
}