Обновить токен без ощущения (весь процесс от бэкенда к интерфейсу)

внешний интерфейс Безопасность
Обновить токен без ощущения (весь процесс от бэкенда к интерфейсу)

предисловие

Обновление токена — необходимая часть внешней безопасности. В этой статье рассказывается, как обновлять токены, не ощущая весь процесс от серверной до внешней части. Код страницы был реализован и пройден лично, пожалуйста, ешьте его с уверенностью. Если вам нужен исходный код, пожалуйста, оставьте сообщение в области комментариев. Слова кодировать не просто, лайк и поддержка! ! !

Во-первых, реализация идеи

Реализовано с помощью длинных и коротких токенов: короткие токены используются для запроса данных приложения, а длинные токены используются для получения новых коротких токенов (длина относится к сроку действия).

2. Бэкенд-дизайн

  • В бэкенде есть два поля, которые хранят длинные и короткие токены соответственно и каждый раз обновляют их.
  • Если срок действия короткого токена истек, вернуть код возврата: 104, если срок действия длинного токена, вернуть код возврата: 108, если запрос выполнен успешно, вернуть код возврата: 0;
  • Пропуск в заголовке запроса используется для получения длинного токена клиента, а авторизация в заголовке запроса используется для получения короткого токена клиента.

1. Построить службу узла

Создайте новую папку, откройте ее с помощью vscode, запустите:

npm init -y

установить коа

npm i koa -s

Новый index.js

const Koa = require('koa')
const app = new Koa();

app.use(async(ctx,next)=>{
    ctx.body = "这是一个应用中间件";
    await next()
})

app.listen(4000,() => {
    console.log('server is listening on port 4000')
})

установить нодмон

npm i nodemon -g

настроить package.json

"dev":"nodemon index.js",

Выполнить: npm run dev Доступ: 127.0.0.1:4000, вы можете видеть, что на странице показано, что это промежуточное программное обеспечение приложения.

2. Используйте промежуточное ПО для маршрутизации

Установить

npm i koa-router -S

Новые маршруты/index.js

const router = require("koa-router")();
let accessToken = "init_s_token"; //短token
let refreshToken = "init_l_token"; //长token

/* 5s刷新一次短token */
setInterval(() => {
  accessToken = "s_tk" + Math.random();
}, 5000);

/* 一小时刷新一次长token */
setInterval(() => {
  refreshToken = "l_tk" + Math.random();
}, 600000);

/* 登录接口获取长短token */
router.get("/login", async (ctx) => {
  ctx.body = {
    returncode: 0,
    accessToken,
    refreshToken,
  };
});

/* 获取短token */
router.get("/refresh", async (ctx) => {
  //接收的请求头字段都是小写的
  let { pass } = ctx.headers;
  if (pass !== refreshToken) {
    ctx.body = {
      returncode: 108,
      info: "长token过期,重新登录",
    };
  } else {
    ctx.body = {
      returncode: 0,
      accessToken,
    };
  }
});

/* 获取应用数据1 */
router.get("/getData", async (ctx) => {
  let { authorization } = ctx.headers;
  if (authorization !== accessToken) {
    ctx.body = {
      returncode: 104,
      info: "token过期",
    };
  } else {
    ctx.body = {
      code: 200,
      returncode: 0,
      data: { id: Math.random() },
    };
  }
});

/* 获取应用数据2 */
router.get("/getData2", async (ctx) => {
  let { authorization } = ctx.headers;
  if (authorization !== accessToken) {
    ctx.body = {
      returncode: 104,
      info: "token过期",
    };
  } else {
    ctx.body = {
      code: 200,
      returncode: 0,
      data: { id: Math.random() },
    };
  }
});

module.exports = router;

Изменить index.js

//删除
app.use(async(ctx,next)=>{
    ctx.body = "这是一个应用中间件";
    await next()
})
//新增
const index = require('./routes/index')
app.use(index.routes(),index.allowedMethods())

3. Междоменная обработка

Установить

npm i koa2-cors

использовать:

const cors = require('koa2-cors');

app.use(cors());

окончательный файл index.js

const Koa = require('koa')
const app = new Koa();
const index = require('./routes/index')

const cors = require('koa2-cors');

app.use(cors());

app.use(index.routes(),index.allowedMethods())

app.listen(4000,() => {
    console.log('server is listening on port 4000')
})

Структура каталога:

Перезапустите npm run dev и сервер готов

3. Интерфейсный дизайн

1. Определите используемые константы

Новый конфиг/constant.js

/* localStorage存储字段 */
export const ACCESS_TOKEN = "s_tk"; //短token
export const REFRESH_TOKEN = "l_tk"; //长token、
/* HTTP请求头字段 */
export const AUTH = "Authorization"; //存放短token
export const PASS = "PASS"; //存放长token

Новый конфиг/returnCodeMap.js

// 在其它客户端被登录
export const CODE_LOGGED_OTHER = 106;
// 重新登陆
export const CODE_RELOGIN = 108;
// token过期
export const CODE_TOKEN_EXPIRED = 104;
//接口请求成功
export const CODE_SUCCESS = 0;

2. Услуги по упаковке

Ключевой момент: хранить запрос с просроченным токеном в массиве с помощью промиса, держать промис в состоянии ожидания (т.е. не вызывать резолв) и перезапрашивать по одному при получении нового короткого токена

Если цепочка обещаний не поддерживается, она будет рассматриваться как новый запрос, и содержимое страницы не будет обновляться.

установить аксиомы

npm i axios -S

Новый сервис/index.js

import axios from "axios";
import { refreshAccessToken, addSubscriber } from "./refresh";
import { clearAuthAndRedirect } from "./clear";
import {
  CODE_LOGGED_OTHER,
  CODE_RELOGIN,
  CODE_TOKEN_EXPIRED,
  CODE_SUCCESS,
} from "../config/returnCodeMap";
import { ACCESS_TOKEN, AUTH } from "../config/constant";

const service = axios.create({
  baseURL: "//127.0.0.1:4000",
  timeout: 30000,
});

service.interceptors.request.use(
  (config) => {
    let { headers } = config;
    const s_tk = localStorage.getItem(ACCESS_TOKEN);
    s_tk &&
      Object.assign(headers, {
        [AUTH]: s_tk,
      });
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

service.interceptors.response.use(
  (response) => {
    let { config, data } = response;
    //retry:第一次请求过期,接口调用refreshAccessToken,第二次重新请求,还是过期则reject出去
    let { retry } = config;
    /* 延续Promise链 */
    return new Promise((resolve, reject) => {
      if (data["returncode"] !== CODE_SUCCESS) {
        if ([CODE_LOGGED_OTHER, CODE_RELOGIN].includes(data.returncode)) {
          clearAuthAndRedirect();
        } else if (data["returncode"] === CODE_TOKEN_EXPIRED && !retry) {
          config.retry = true;
          addSubscriber(() => resolve(service(config)));
          refreshAccessToken();
        } else {
          return reject(data);
        }
      } else {
        resolve(data);
      }
    });
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default service;

Новый сервис/refresh.js

import service from "./index";
import { ACCESS_TOKEN, REFRESH_TOKEN, PASS } from "../config/constant";
import { clearAuthAndRedirect } from "./clear";

let subscribers = [];
let pending = false; //同时请求多个过期链接,保证只请求一次获取短token

export const addSubscriber = (request) => {
  subscribers.push(request);
};

export const retryRequest = () => {
  subscribers.forEach((request) => request());
  subscribers = [];
};

export const refreshAccessToken = async () => {
  if (!pending) {
    try {
      pending = true;
      const l_tk = localStorage.getItem(REFRESH_TOKEN);
      if (l_tk) {
        /* 重新获取短token */
        const { accessToken } = await service.get(
          "/refresh",
          Object.assign({}, { headers: { [PASS]: l_tk } })
        );
        localStorage.setItem(ACCESS_TOKEN, accessToken);
        retryRequest();
      }
      return;
    } catch (e) {
      clearAuthAndRedirect();
      return;
    } finally {
      pending = false;
    }
  }
};

Новый сервис/clear.js

import {ACCESS_TOKEN} from '../config/constant'

/* 清除长短token,并定位到登录页(在项目中使用路由跳转) */
export const clearAuthAndRedirect = () =>{
    localStorage.removeItem(ACCESS_TOKEN)
    window.location.href = '/login'
}

3. Используйте

React используется здесь для достижения того же эффекта, что и при использовании vue.

//App.js

import { useState } from "react";import service from "./service/index.js";import { ACCESS_TOKEN, REFRESH_TOKEN } from "./config/constant";const App = () => {  const [data1, setData1] = useState();  const [data2, setData2] = useState();  const getData = () => {    service.get("/getData").then((res) => {      setData1(res.data.id);    });    service.get("/getData2").then((res) => {      setData2(res.data.id);    });  };  const getToken = () => {    service.get("/login").then((res) => {      //存储长token      localStorage.setItem(REFRESH_TOKEN, res.refreshToken);      //存储短token      localStorage.setItem(ACCESS_TOKEN, res.accessToken);    });  };  return (    <div>      {data1}--{data2}      <button onClick={getData}>按钮</button>      <button onClick={getToken}>登录</button>    </div>  );};export default App;

Запустите проект, чтобы увидеть эффект

Сначала нажмите кнопку входа в систему, чтобы получить длинные и короткие токены, а затем нажмите кнопку через 5 секунд, чтобы получить данные приложения (вышеупомянутая серверная часть устанавливает срок действия короткого токена 5s).

Данные страницы могут быть обновлены, а интерфейс обновления вызывается только один раз.