记录一下为 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
|
import axios from "axios";
const baseURL = process.env.VUE_APP_BASE_API;
const timeout = 300 * 1000;
const headers = { "Content-Type": "application/json" };
const withCredentials = true;
let isRefreshing = false;
let failedQueue = [];
const axiosIns = axios.create({ baseURL, timeout, headers, withCredentials, });
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) { 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) => {
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
|
use thans\jwt\exception\JWTException; use thans\jwt\facade\JWTAuth; use think\facade\Cookie; use think\Request;
class Auth extends BaseController {
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成功'); }
private function setUserCookie($token) { Cookie::delete('token');
$tokenOption = [ 'expire' => env('JWT_REFRESH_TTL') * 60 * 2, 'httponly' => true, 'secure' => true, 'samesite' => 'Strict', ];
$isDebug = env('app_debug'); if ($isDebug) { $tokenOption['httponly'] = false; $tokenOption['secure'] = false; $tokenOption['samesite'] = 'Lax'; }
Cookie::set('token', $token, $tokenOption); } }
|
后续
如果你想实现传统的双 token 认证,而不是我上面介绍的这种特殊的双 token 认证,你可以直接生成两个 token,这个过程就不再赘叙了。