为thinkphp6.1添加双token认证

木头的喵喵拖孩

记录一下为 thinkphp6.1 添加双 token 认证的过程

项目环境介绍

我的项目是前后端分离的 thinkphp6.1 框架项目,php 版本为 7.4,
后端已经使用了thans/tp-jwt-auth 库提供的单 token 认证功能,
前端使用 Axios 来进行数据请求,会将 token 放在自定义请求头中。

库介绍

前端不需要新增库,只需要稍微修改使用 Axios 的逻辑即可。

后端使用thans/tp-jwt-auth 库提供的刷新 token API 来实现双 token 认证功能。

思路

这里要强调一下,传统的双 token 认证是在用户登陆时生成两个 token,一个用于验证用户的 accessToken(访问用 token,生存时间短,以分钟为单位),一个用于刷新 accessToken 的 refreshToken(刷新用 token,生存时间长,以天为单位)。
前端访问后端的普通的接口时使用 accessToken 来鉴权,当 accessToken 过期时,就需要将 refreshToken 发送给专用的刷新接口来重新生成 accessToken(也可以不需要专用的刷新接口,而在后端实现无感刷新,这里不介绍)。

但是我这里说的双 token 认证和传统始的 token 认证是有区别的,因为thans/tp-jwt-auth 库已经提供一个刷新 token 的 API,所以我们在用户登录时只需要创建一个 token,即 accessToken,此时 refreshToken 已经被包含在 accessToken 的 payload 中,所以不需要单独创建 refreshToken。

为了安全起见,该 accessToken 会在登陆时,由后端直接 Set-Cookie,并且设置 httponly=true、secure=true、samesite=strict,不需要前端来管理存储。

前端访问后端接口时,需要携带存放了 accessToken 的 cookie,当访问普通的接口返回 401 时,就需要去访问专用的刷新接口来重新生成 accessToken。

和传统的双 token 认证不一样的是:这里不管是访问普通接口还是刷新接口,都是只需要携带一个 token,即 accessToken,因为 refreshToken 已经被包含在 accessToken 的 payload 中,所以不需要单独发送 refreshToken。后端解析 accessToken 时,会判断 payload 中的 refreshToken 是否过期,从而根据条件来返回 401 或者刷新 accessToken。

还有一处不一样的是,在 refreshToken 的过期时间内调用thans/tp-jwt-auth 库的刷新 token 的 API 后,连带着 refreshToken 也会被刷新,
就是说,如果你的 refreshToken 的生存时间是 7 天,那么在 7 天之内调用刷新接口,refreshToken 的生存时间也会被重置为 7 天。逻辑上来讲,如果你一直是 7 天内刷新 token,那么理论上你可以永远不再手动登录。

前端修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 修改request.js

import axios from "axios";

const baseURL = process.env.VUE_APP_BASE_API;

/**
* 请求超时时间
*/
const timeout = 300 * 1000;

/**
* 默认请求头
*/
const headers = { "Content-Type": "application/json" };

/**
* 是否携带Cookie
*/
const withCredentials = true;

/**
* 创建一个标志,防止多个请求同时触发刷新
*/
let isRefreshing = false;

/**
* 存储刷新过程中堆积的请求
*/
let failedQueue = [];

const axiosIns = axios.create({
baseURL,
timeout,
headers,
withCredentials,
});

/**
* 处理队列中等待的请求
* @param {Error|String} error
* @param {String} token 因为token已经被cookie管理,所以这个参数完全可以忽略
*/
function processQueue(error, token = null) {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
}

/**
* 关键修改,在响应拦截器中处理刷新逻辑
*/
axiosIns.interceptors.response.use(
(response) => {
return response.data;
},
async (error) => {
console.error(error);

const originalRequestConfig = error.config;
const status = error.response?.status ?? error.status ?? -1;
const errorMsg =
error.response?.data?.message ?? error.message ?? "未知错误";

if (status === 401 && !originalRequestConfig._retry) {
// 如果是刷新Token请求本身失败了,直接跳登录页
if (originalRequestConfig.url.includes("auth/refreshToken")) {
router.push({
name: "Login",
});

message.error(`状态码:${status}${errorMsg}`);
return error;
}

// 标记这个请求,防止无限循环
originalRequestConfig._retry = true;

// 如果正在刷新,将当前请求加入队列等待
if (isRefreshing) {
return new Promise((resolve, reject) => {
/**
* 这个地方为每个刷新期间发送的请求添加一个等待控制(将resolve和reject方法放到等待队列中),
* 当刷新token成功后,会依次调用队列中的resolve或者reject方法,以此实现等待的逻辑
*/
failedQueue.push({ resolve, reject });
})
.then(async (token) => {
try {
let res = await axiosIns(originalRequestConfig);
return Promise.resolve(res.data);
} catch (err) {
return Promise.reject(err);
}
})
.catch((err) => {
return Promise.reject(err);
});
}

isRefreshing = true;

// 尝试调用专用的刷新接口
try {
const response = await axiosIns.post("/auth/refreshToken");

const newToken = response.data.token;

// 处理队列中等待的请求
processQueue(null, newToken);

// 重新发送原始的请求
let res = await axiosIns(originalRequestConfig);
return Promise.resolve(res.data);
} catch (refreshError) {
// 刷新也失败,引导用户重新登录
processQueue(refreshError, null);
router.push({
name: "Login",
});
return error;
} finally {
isRefreshing = false;
}
} else {
message.error(`状态码:${status}${errorMsg}`);
return error;
}
}
);

后端修改

1
2
3
4
5
6
7
8
# 修改根目录下的.env文件,添加如下配置

[JWT]
SECRET=你的JWT密钥
# 单位:秒
TTL=7200
# 单位:分钟
REFRESH_TTL=20160
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 修改 Auth.php 控制器

use thans\jwt\exception\JWTException;
use thans\jwt\facade\JWTAuth;
use think\facade\Cookie;
use think\Request;

class Auth extends BaseController {

/**
* Summary of refreshToken
* 刷新token
* @return \think\Response
*/
public function refreshToken()
{
$token = null;
try {
$token = JWTAuth::refresh();
} catch (JWTException $e) {
return fJsonError('登录过期,' . $e->getMessage(), 401);
}

$this->setUserCookie($token);

return fJsonSuccess(['token' => $token], '刷新token成功');
}

/**
* Summary of setUserCookie
* 设置用户cookie
* @param mixed $token
* @return void
*/
private function setUserCookie($token)
{
Cookie::delete('token');

$tokenOption = [
'expire' => env('JWT_REFRESH_TTL') * 60 * 2, // 使cookie的有效期是refreshToken有效期的2倍,防止refreshToken过期和cookie过期同时触发
'httponly' => true, // 使cookie只能通过http协议读取,无法通过js读取
'secure' => true, // 使cookie只能通过https协议访问,不能通过http协议访问
'samesite' => 'Strict', // 使cookie只能通过当前域名访问,不能通过其他域名访问
];

// 测试环境
$isDebug = env('app_debug');
if ($isDebug) {
$tokenOption['httponly'] = false;
$tokenOption['secure'] = false;
$tokenOption['samesite'] = 'Lax';
}

Cookie::set('token', $token, $tokenOption);
}
}

后续

如果你想实现传统的双 token 认证,而不是我上面介绍的这种特殊的双 token 认证,你可以直接生成两个 token,这个过程就不再赘叙了。

  • 标题: 为thinkphp6.1添加双token认证
  • 作者: 木头的喵喵拖孩
  • 创建于: 2025-12-19 14:14:09
  • 更新于: 2025-12-22 17:35:41
  • 链接: https://blog.xx-xx.top/2025/12/19/为thinkphp6-1添加双token认证/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
为thinkphp6.1添加双token认证