{"id":21129545,"url":"https://github.com/chengpeiquan/refresh-token","last_synced_at":"2025-10-26T23:04:32.577Z","repository":{"id":38842698,"uuid":"325745828","full_name":"chengpeiquan/refresh-token","owner":"chengpeiquan","description":"The refreshToken scheme and demo based on OAuth 2.0 for Front end developer.","archived":false,"fork":false,"pushed_at":"2022-06-02T07:25:21.000Z","size":856,"stargazers_count":11,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-01T09:43:21.172Z","etag":null,"topics":["refresh-token","refresh-tokens","refreshtoken"],"latest_commit_sha":null,"homepage":"","language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chengpeiquan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-12-31T07:46:28.000Z","updated_at":"2023-10-30T08:09:32.000Z","dependencies_parsed_at":"2022-09-18T12:51:04.788Z","dependency_job_id":null,"html_url":"https://github.com/chengpeiquan/refresh-token","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chengpeiquan/refresh-token","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chengpeiquan%2Frefresh-token","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chengpeiquan%2Frefresh-token/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chengpeiquan%2Frefresh-token/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chengpeiquan%2Frefresh-token/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chengpeiquan","download_url":"https://codeload.github.com/chengpeiquan/refresh-token/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chengpeiquan%2Frefresh-token/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274174271,"owners_count":25235203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["refresh-token","refresh-tokens","refreshtoken"],"created_at":"2024-11-20T05:25:23.197Z","updated_at":"2025-10-26T23:04:32.505Z","avatar_url":"https://github.com/chengpeiquan.png","language":"Vue","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 基于 OAuth 2.0 的 refreshToken 方案与 demo\n\n如今在涉及到用户登录的系统设计里面，基本上都是通过 OAuth 2.0 来设计授权，当你在调用登录接口的时候，可以看到在返回来的数据里面会有 2 个 Token：一个 `accessToken` 和一个 `refreshToken` 。\n\n为什么会有两个 Token，之间有什么区别？这其实是 [OAuth 2.0 的四种方式](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) 之一的 “凭证式”，一个是平时请求接口时的用户凭证，一个是用来刷新用户凭证的刷新凭证。\n\n这也是我最近在业务上涉及到的一处开发需求点，之前的老业务，服务端都没有按照这样的模式去做，单纯的过期就让用户重新登录，所以自己也没有实际去处理过 Token 续期的场景。\n\n一波处理下来，刚开始下手觉得有点繁琐，但实现起来还是蛮简单的，过程颇觉有趣，把第一次的开发经验记录起来。\n\n## 需求背景\n\n通常来说下发的 `accessToken` 都有一个比较短暂的有效期，大部分情况下可能只有大半天，短的话更可能只有 2 ~ 3 小时（对，我处理的这个业务就是……），意味着用户在一天之内可能需要频繁进行重新登录。\n\n关于为什么 `accessToken` 的有效期要那么短，可以参考 [OAuth 2.0 的一个简单解释](http://www.ruanyifeng.com/blog/2019/04/oauth_design.html) 。\n\n传统的登录都是到期了跳回登录页面，让用户重新走一遍登录流程就可以了，但如今 `accessToken` 的超短有效期带来的用户体验是非常糟糕的，为了安全而牺牲用户体验，就是产品和开发打架的常见原因之一。\n\n那么有没有办法既保证安全，又能够减少用户重复登录的操作呢？ `refreshToken` 就是因此产生。\n\n它可以用来请求重新颁发一个 `accessToken`，当请求被告知过期时，通过刷新令牌的方式，用新的令牌来完成之前还没完成的请求，让用户可以不重新登录，达到无感知刷新的目的，直到 `refreshToken` 也过期了，才需要回去走登录流程。\n\n这一篇来讲一讲如何无感知的帮助用户执行 `accessToken` 的刷新。\n\n## 需求目的\n\n搞清楚目的才能好好搞事情哈哈哈，于是拆解了一下需求，分为三个小点：\n\n1. 当 `accessToken` 过期的时候，在发起下一次请求之前，前端先帮用户主动刷新 Token，拿到新的 Token 完成后续的请求\n\n2. 在刷新 Token 成功之前，不允许重复刷新（因为一个页面可能有多个请求），多个未完成的请求需要挂起\n\n3. 当 `refreshToken` 也过期时（也就是刷新失败），停止重复刷新，引导用户重新登录\n\nBtw: 后面的 Token 统一都是指 `accessToken` 。\n\n## 实现思路\n\n理清楚需求目的之后，还需要先跟服务端同学约定一下判断规则，先确认我们在前端能够拿到哪些数据，按照上一次对接的业务情况，服务端的登录接口提供了以下三个字段返回：\n\n字段|含义\n:--|:--\naccessToken|请求接口的时候，需要在请求头里带上的 Token\nrefreshToken|用来请求刷新 Token 的凭证\nexpiresTime|Token 的过期时间\n\n其中登录接口和刷新接口是免 Token 验证的，登录接口只需要校验默认的请求头以及账号密码，刷新接口只需要校验刷新凭证。\n\n## 实现过程\n\n以 Vue + Axios 来搭一个演示项目为例，核心代码相关的文件是这几个：\n\n```html\nsrc\n└─libs\n  ├─axios\n  │ ├─config.ts\n  │ ├─index.ts\n  │ └─instance.ts\n  ├─refreshToken.ts\n  └─setLoginInfoToLocal.ts\n```\n\n虽然文件比较多，但代码其实不多，习惯把一些可能复用的代码抽离出来独立成模块了。\n\n文件|作用\n:--|:--\naxios/config.ts|axios 的一些基础配置，可以配置接口路径、超时时间等\naxios/instance.ts|一个 axios 实例，在这里配置了一些全局都会用到的请求拦截、返回拦截\naxios/index.ts|组件里用到的 axios 入口文件，会在这里再添加一些专属业务侧的拦截\nrefreshToken.ts|用来刷新 Token 的一些业务代码，返回一个 Promise\nsetLoginInfoToLocal.ts|存储登录信息到本地，在调用登录接口和刷新接口之后需要用到\n\n点击查看： [libs - refresh-token](https://github.com/chengpeiquan/refresh-token/tree/main/src/libs)\n\n下面把几个主要文件里面，主要的代码部分讲一下：\n\n### config.ts\n\n之所以要抽离出 config ，是因为之前遇到一个坑，axios 如果先 create 再 export，那么用到的地方其实都是同一个实例，不同的模块里引用了同一个实例然后还要再做一些拦截，会相互覆盖。\n\n所以如果你在其他地方，可能要用到一个干净的新实例的时候，抽离出 config 可以单独 create ，可以减少你重复编写代码的情况。\n\n你在这里可以动态指定接口路径、默认的请求头、超时时间等等。\n\n```ts\nconst config: any = {\n\n  // 接口路径\n  baseURL: IS_DEV \n    ?\n    'http://127.0.0.1:12321/api'\n    :\n    'https://www.fastmock.site/mock/1c85c0d436ae044cf22849549ef471b8/api',\n\n  // 公共请求头\n  headers: {\n    'Content-Type': 'application/json; charset=UTF-8',\n    Authorization: 'Basic KJytrqad8765Fia'\n  },\n\n  // 默认的响应方式\n  responseType: 'json',\n\n  // 超时时间\n  timeout: 30000, \n\n  // 跨域的情况下不需要带上cookie\n  withCredentials: false,\n\n  // 调整响应范围，范围内的可以进入then流程，否则会走catch\n  validateStatus: (status: number) =\u003e {\n    return status \u003e= 200 \u0026\u0026 status \u003c 500;\n  }\n\n}\n```\n\n完整代码：[config.ts - refresh-token](https://github.com/chengpeiquan/refresh-token/blob/main/src/libs/axios/config.ts)\n\n官方文档：[请求配置 - axios](http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE)\n\n### instance.ts\n\n单独封装的 `instance`，是一个 “干净” 的实例，它里面包含的只是全局都会用到的一些请求拦截和返回拦截。\n\n请求拦截可以在开始请求之前，添加上一些特殊数据，比如给每个请求头都带上 Token 等等。\n\n```ts\ninstance.interceptors.request.use(\n\n  // 正常拦截\n  config =\u003e {\n    \n    // 添加token\n    const LOCAL_TOKEN: string = ls.get('token') || '';\n    if ( LOCAL_TOKEN ) {\n      config.headers['Authorization'] = LOCAL_TOKEN;\n    }\n\n    // 返回处理后的配置\n    return Promise.resolve(config);\n  },\n  \n  // 拦截失败\n  err =\u003e Promise.reject(err)\n\n);\n```\n\n返回拦截可以拦截掉一些特殊的返回情况，还可以简化接口返回的数据等等。\n\n```ts\ninstance.interceptors.response.use(\n\n  // 正常响应\n  res =\u003e {\n\n    // 处理axios在IE 8-9下的坑爹问题\n    if (\n      res.data === null\n      \u0026\u0026\n      res.config.responseType === 'json'\n      \u0026\u0026\n      res.request.responseText !== null\n    ) {\n\n      try {\n        res.data = JSON.parse(res.request.responseText);\n      }\n      catch (e) {\n        console.log(e);\n      }\n\n    }\n\n    // 登录失效拦截（主要针对refreshToken也失效的情况）\n    if ( res.data.code === 1 \u0026\u0026 res.data.msg === '用户凭证已过期' ) {\n      \n      // 告知用户\n      message.error(res.data.msg);\n\n      // 切去登录\n      try {\n        router.push({\n          name: 'login'\n        });\n      }\n      catch (e) {\n        console.log(e);\n      }\n\n    }\n\n    // 提取接口的返回结果，简化接口调用的编码操作\n    return Promise.resolve(res.data);\n  },\n\n  // 异常响应（统一返回一个msg提示即可）\n  err =\u003e Promise.reject('网络异常')\n\n);\n\nexport default instance;\n```\n\n完整代码：[instance.ts - refresh-token](https://github.com/chengpeiquan/refresh-token/blob/main/src/libs/axios/instance.ts)\n\n### index.ts\n\n其实和 `instance.ts` 的性质差不多，本质上也是要在这里做一些拦截，但是不同于 `instance` 的地方在于，入口文件更多的是侧重于业务侧的拦截。\n\n比如前面有说到，登录接口和刷新接口是不需要校验用户凭证的，也就是不必每个接口都需要进行 Token 刷新，那么这些只针对部分业务接口的拦截，就统一放到 `index` 这边。\n\n我们的刷新操作也是在这里完成的。\n\n我们前面说到，在拦截的时候，要做到不允许重复刷新，同时多个未完成的请求需要挂起，所以我们需要定义两个全局变量。\n\n```ts\n// 防止重复刷新的状态开关\nlet isRefreshing: boolean = false;\n\n// 被拦截的请求列表\nlet requests: any[] = [];\n```\n\n前端主动发起刷新的判断标准，就是看本地记录的时间是否到期，所以要先检测本地是否存在时间记录，计算时间差：\n\n```ts\n// 读取Token的过期时间戳\nconst OLD_TOKEN_EXP: number = ls.get('token_expired_timestamp') || 0;\n\n// 获取当前的时间戳\nconst NOW_TIMESTAMP: number = Date.now();\n\n// 计算剩余时间\nconst TIME_DIFF: number = OLD_TOKEN_EXP - NOW_TIMESTAMP;\n```\n\n同时还要检查是否具备主动发起刷新的条件，必须本地存在旧的记录，才会去帮用户刷新。\n\n```ts\n// 是否有Token存储记录\nconst HAS_LOCAL_TOKEN: boolean = ls.get('token') ? true : false;\n\n// 是否有Token过期时间记录\nconst HAS_LOCAL_TOKEN_EXP: boolean = OLD_TOKEN_EXP ? true : false;\n```\n\n然后因为像刷新请求这个请求不应该触发刷新，所以再获取一下接口的 URL：\n\n```ts\n// 获取接口url\nconst API_URL: string = config.url || '';\n```\n\n最后，我们要把刷新操作都放到综合条件里面去，满足所有条件的，才去执行刷新。\n\n```ts\nif (\n  API_URL !== '/refreshToken'\n  \u0026\u0026\n  HAS_LOCAL_TOKEN\n  \u0026\u0026\n  HAS_LOCAL_TOKEN_EXP\n  \u0026\u0026\n  TIME_DIFF \u003c= 0\n) {\n  // 这里面是刷新的操作...\n}\n```\n\n开始刷新的时候，为了避免重复刷新，只有未刷新时，才会进入刷新流程，同时进入后需要先把状态打开。\n\n然后获取新的 Token，拿到新的 Token 之后，再把原来挂起的请求执行掉，在这里记得重置队列，避免队列越来越多，下次刷新时造成无畏的重复请求。\n\n```ts\n// 如果没有在刷新，则执行刷新\nif ( !isRefreshing ) {\n\n  // 打开状态\n  isRefreshing = true;\n\n  // 获取新的token\n  const NEW_TOKEN: string = await refreshToken();\n\n  // 如果新的token存在，用新token继续之前的请求，然后重置队列\n  if ( NEW_TOKEN ) {\n    config.headers['Authorization'] = NEW_TOKEN;\n    requests.forEach( (callback: any) =\u003e callback(config) );\n    requests = [];\n  }\n  // 否则直接清空队列，因为需要重新登录了\n  else {\n    requests = [];\n  }\n\n  // 关闭状态，允许下次继续刷新\n  isRefreshing = false;\n\n}\n```\n\n配合上一步，我们需要把刷新 Token 成功之前的请求都挂起来，因为 `Promise` 只有当 `resolve` 或者 `reject` 的时候才会返回结果，所以我们在 `Promise` 里，把请求都先丢到 `requests` 数组里存起来，就能达到请求挂起的目的。\n\n```ts\n// 并把刷新完成之前的请求都存储为请求队列\nreturn new Promise( (resolve: any) =\u003e {\n  requests.push( () =\u003e {\n    resolve(config)\n  });\n});\n```\n\n完整代码：[index.ts - refresh-token](https://github.com/chengpeiquan/refresh-token/blob/main/src/libs/axios/index.ts)\n\n### refreshToken.ts\n\n在 `index` 里有一个操作是拿到刷新后的 Token：\n\n```ts\n// 获取新的token\nconst NEW_TOKEN: string = await refreshToken();\n```\n\n这里其实是一个接口请求操作，就是通过登录时给的 `refreshToken` ，去请求刷新凭证的接口签发新的 `accessToken` 下来。\n\n为了减少代码的回调，方便 `index` 采用 `async / await`，所以这里需要返回一个 `Promise`，当刷新成功时，返回新的 Token 字符串，刷新失败则返回空的字符串。\n\n```ts\nconst refreshToken = (): Promise\u003cany\u003e =\u003e {\n  return new Promise( resolve =\u003e {\n    \n    // 获取本地记录的刷新凭证\n    const REFRESH_TOKEN: string = ls.get('refresh_token') || '';\n\n    // 请求刷新\n    axios({\n      method: 'post',\n      url: '/refreshToken',\n      data: {\n        refreshToken: REFRESH_TOKEN\n      }\n    }).then( (data: any) =\u003e {\n      \n      // 存储token信息\n      const DATA: any = data.data;\n      setLoginInfoToLocal(DATA);\n\n      // 返回新的token，通知那边搞定了\n      const NEW_TOKEN: string = `${DATA.tokenType} ${DATA.accessToken}`;\n      resolve(NEW_TOKEN);\n\n    }).catch( (msg: string) =\u003e {\n      resolve('');\n    });\n    \n  });\n}\n```\n\n在这里，刷新完毕后，记得同时把新的资料存储到本地去，更新上次登录记录的那些数据，所以我才要把 `setLoginInfoToLocal` 的操作抽离出来，减少重复代码的编写。\n\n完整代码：[refreshToken.ts - refresh-token](https://github.com/chengpeiquan/refresh-token/blob/main/src/libs/refreshToken.ts)\n\n## 项目演示\n\n这篇文章对应的仓库就是一个项目源码，这里提供了两种类型的 Mock 接口：\n\n### 本地 Express Server\n\n推荐用这个方式，可以一边测试效果，一边看代码。\n\n1. 先通过 `git clone https://github.com/chengpeiquan/refresh-token.git` 克隆本仓库到本地\n\n2. 控制台访问项目，输入 `npm install` 执行依赖安装\n\n3. 执行 `npm run api` 启动接口服务\n\n4. 另外打开一个控制台访问项目，输入 `npm run serve` 启动项目调试\n\n你可以在 `service` 文件夹里修改接口的一些返回数据，比如 Token 的有效期（目前默认都是 5s 过期，方便测试），以及 refreshToken 的有效几率（因为无法校验刷新凭证的合法性，所以目前采用的是随机生成一个布尔值，当 `false` 的时候表示刷新凭证过期，`true` 则允许继续刷新），等等。\n\n```html\nservice\n├─api\n│ ├─login.js\n│ ├─refreshToken.js\n│ └─test.js\n├─createApi.js\n└─index.js\n```\n\n这些文件的说明：\n\n文件|作用\n:--|:--\nindex.js|服务的启动入口文件\ncreateApi.js|创建接口的文件，可以把写好的接口导进来生成\napi文件夹|里面存放的是接口文件，一个文件对应一个接口\n\n### 远程 FastMock API\n\n原本是采用这个方式的，但是可能受自己网络或者对方服务器影响，有时候响应很慢，试过 30s 超时了还没响应回来，花费过多时间在等待上了，所以才换成了本地 Server 。\n\n线上访问：[Refresh Token Demo](https://chengpeiquan.github.io/refresh-token/)\n\n你也可以创建自己的 FastMock 接口，登录官网进行配置后，修改 `src/libs/axios/config.ts` 里的 `baseURL` 。\n\n点击访问：[FastMock 官网](https://www.fastmock.site/)\n\n点击访问：[FastMock 操作文档](https://marvengong.gitee.io/fastmock/)\n\n## 参考资料\n\n感谢各位大神总结的相关知识点，收益很多，才有了自己的一番实践和总结，建议大家有兴趣也可以阅读一下！\n\n[理解OAuth 2.0](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)\n\n[OAuth 2.0 的四种方式](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html)\n\n[深入理解token](https://www.cnblogs.com/xuxinstyle/p/9675541.html)\n\n[请求时token过期自动刷新token](https://segmentfault.com/a/1190000016946316)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchengpeiquan%2Frefresh-token","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchengpeiquan%2Frefresh-token","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchengpeiquan%2Frefresh-token/lists"}