Ant-design-pro: 404 500 等错误画面跳转问题 以及用户token失效后统一跳转到登陆画面的问题

Created on 27 Nov 2017  ·  16Comments  ·  Source: ant-design/ant-design-pro

请问 如何在request.js文件中 实现路由跳转,希望通过response的状态 来跳转到相应的画面,例如500 统一跳转到500画面,希望能统一处理

同时类似用户token在后端缓存失效后 再调用任何接口都应统一跳转到登陆画面 因为dva subscriptions功能无法订阅state 所以 如何做共同处理呢
谢谢

🏆 Feature Request

Most helpful comment

我的一些思路:

用户 token 管理思路:

  • ~/src/utils/ 增加 token.js
  • ~/src/models/login.js login 方法里,把后端返回的 token 通过 token.js 里的方法 save 到 sessionStorage。(注:用 sessionStorage 好处是用户关闭页面时,sessionStorage 的值也随着删除)
  • ~/src/utils/request.js 发请求时,通过 header 头把 token 携带到后端进行验证
login.js

import { routerRedux } from 'dva/router';
import { fakeAccountLogin } from '../services/api';
import token from '../utils/token';

export default {
  namespace: 'login',

  state: {
    status: undefined,
  },

  effects: {
    *login({ payload }, { call, put }) {
      yield put({
        type: 'changeSubmitting',
        payload: true,
      });
      const response = yield call(fakeAccountLogin, payload);
      yield put({
        type: 'changeLoginStatus',
        payload: response,
      });
      // Login successfully
      token.save(response.token);

      if (response.status === 'ok') {
        yield put(routerRedux.push('/'));
      }
    },
    *logout(_, { put }) {

      // remove token in sessionStorage
      token.remove()

      yield put({
        type: 'changeLoginStatus',
        payload: {
          status: false,
        },
      });
      yield put(routerRedux.push('/user/login'));
    },
  },

  reducers: {
    changeLoginStatus(state, { payload }) {
      return {
        ...state,
        status: payload.status,
        type: payload.type,
        submitting: false,
      };
    },
    changeSubmitting(state, { payload }) {
      return {
        ...state,
        submitting: payload,
      };
    },
  },
};


``` js
token.js

import atob from 'atob';
import _ from 'lodash';

const STORAGE_TOKEN_NAME = 'TOKEN';

/**

  • JWT的方案
    */
    export default {
    parse() {
    let token = this.get();
    try {
    const arr = token.split('.');
    if (arr.length === 3) {
    token = atob(token.split('.')[1]);
    }
    return JSON.parse(token);
    } catch (ex) {
    throw ex;
    }
    },
    check() {
    try {
    const payload = this.parse();
    return !_.isEmpty(payload);
    } catch (ex) {
    this.remove();
    return false;
    }
    },
    get() {
    return sessionStorage.getItem(STORAGE_TOKEN_NAME);
    },
    save(token) {
    sessionStorage.setItem(STORAGE_TOKEN_NAME, token);
    },
    remove() {
    sessionStorage.removeItem(STORAGE_TOKEN_NAME);
    },
    };
``` js request.js
import fetch from 'dva/fetch';
import token from './token';

const codeMessage = {
  200: '服务器成功返回请求的数据',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据,的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器',
  502: '网关错误',
  503: '服务不可用,服务器暂时过载或维护',
  504: '网关超时',
};


function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const errortext = codeMessage[response.status] || response.statusText;
  const error = new Error(errortext);
  error.response = response;
  throw error;
}

function buildAuthorization  ()  {
  const tokenVal = token.get();
  return (token !== '') ? `Bearer ${tokenVal}` : '';
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
 */
export default function request(url, options) {
  const defaultOptions = {
    credentials: 'include',
  };
  const newOptions = { ...defaultOptions, ...options };
  if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json; charset=utf-8',
      ...newOptions.headers,
    };
    newOptions.headers.Authorization = buildAuthorization(); // 增加的代码
    newOptions.body = JSON.stringify(newOptions.body);
  }

  return fetch(url, newOptions)
    .then(checkStatus)
    .then((response) => {
      if (newOptions.method === 'DELETE' || response.status === 204) {
        return response.text();
      }
      return response.json();
    });
}

用户 token 在后端验证失败时的实现思路:

  • 后端的实现:对前端请求携带的 token 进行验证,如果无效时,直接响应401状态码。
  • 前端的实现:在 入口文件 ~/src/index.js 进行全局异常捕捉,把 ~/src/utils/request.js 里的 notification 提醒移到 ~/src/index.js
import 'babel-polyfill';
import dva from 'dva';
import { routerRedux } from 'dva/router';
import { notification } from 'antd';
import 'moment/locale/zh-cn';
import './g2';
import './rollbar';
// import browserHistory from 'history/createBrowserHistory';
import './index.less';

// 1. Initialize
const app = dva({
  onError(err, dispatch) {
    const { response, message } = err;
    const { status, url } = response;
    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: message,
    });
    if (status === 401) {
      dispatch(routerRedux.push('/user/login'));
    }
  },
  // history: browserHistory(),
});

// 2. Plugins
// app.use({});

// 3. Register global model
app.model(require('./models/global'));

// 4. Router
app.router(require('./router'));

// 5. Start
app.start('#root');

All 16 comments

实际业务中,也碰到需要统一进行 token 等异常请求判断的场景,关注~

目前有这种需求,临时解决方案是粗暴跳转

关注 希望大大能做个JWT管理的模块

@lazy1523 你可以详细描述一下需求..

关注中。jwt的方案比较能减轻服务器多用户压力。

感觉这个ant-design-pro还没有vue-adminTemplate完善啊,如何统一处理401/403,如何处理权限,完全没有涉及到。。

正在完善!请耐心等待

我的一些思路:

用户 token 管理思路:

  • ~/src/utils/ 增加 token.js
  • ~/src/models/login.js login 方法里,把后端返回的 token 通过 token.js 里的方法 save 到 sessionStorage。(注:用 sessionStorage 好处是用户关闭页面时,sessionStorage 的值也随着删除)
  • ~/src/utils/request.js 发请求时,通过 header 头把 token 携带到后端进行验证
login.js

import { routerRedux } from 'dva/router';
import { fakeAccountLogin } from '../services/api';
import token from '../utils/token';

export default {
  namespace: 'login',

  state: {
    status: undefined,
  },

  effects: {
    *login({ payload }, { call, put }) {
      yield put({
        type: 'changeSubmitting',
        payload: true,
      });
      const response = yield call(fakeAccountLogin, payload);
      yield put({
        type: 'changeLoginStatus',
        payload: response,
      });
      // Login successfully
      token.save(response.token);

      if (response.status === 'ok') {
        yield put(routerRedux.push('/'));
      }
    },
    *logout(_, { put }) {

      // remove token in sessionStorage
      token.remove()

      yield put({
        type: 'changeLoginStatus',
        payload: {
          status: false,
        },
      });
      yield put(routerRedux.push('/user/login'));
    },
  },

  reducers: {
    changeLoginStatus(state, { payload }) {
      return {
        ...state,
        status: payload.status,
        type: payload.type,
        submitting: false,
      };
    },
    changeSubmitting(state, { payload }) {
      return {
        ...state,
        submitting: payload,
      };
    },
  },
};


``` js
token.js

import atob from 'atob';
import _ from 'lodash';

const STORAGE_TOKEN_NAME = 'TOKEN';

/**

  • JWT的方案
    */
    export default {
    parse() {
    let token = this.get();
    try {
    const arr = token.split('.');
    if (arr.length === 3) {
    token = atob(token.split('.')[1]);
    }
    return JSON.parse(token);
    } catch (ex) {
    throw ex;
    }
    },
    check() {
    try {
    const payload = this.parse();
    return !_.isEmpty(payload);
    } catch (ex) {
    this.remove();
    return false;
    }
    },
    get() {
    return sessionStorage.getItem(STORAGE_TOKEN_NAME);
    },
    save(token) {
    sessionStorage.setItem(STORAGE_TOKEN_NAME, token);
    },
    remove() {
    sessionStorage.removeItem(STORAGE_TOKEN_NAME);
    },
    };
``` js request.js
import fetch from 'dva/fetch';
import token from './token';

const codeMessage = {
  200: '服务器成功返回请求的数据',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据,的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器',
  502: '网关错误',
  503: '服务不可用,服务器暂时过载或维护',
  504: '网关超时',
};


function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const errortext = codeMessage[response.status] || response.statusText;
  const error = new Error(errortext);
  error.response = response;
  throw error;
}

function buildAuthorization  ()  {
  const tokenVal = token.get();
  return (token !== '') ? `Bearer ${tokenVal}` : '';
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
 */
export default function request(url, options) {
  const defaultOptions = {
    credentials: 'include',
  };
  const newOptions = { ...defaultOptions, ...options };
  if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json; charset=utf-8',
      ...newOptions.headers,
    };
    newOptions.headers.Authorization = buildAuthorization(); // 增加的代码
    newOptions.body = JSON.stringify(newOptions.body);
  }

  return fetch(url, newOptions)
    .then(checkStatus)
    .then((response) => {
      if (newOptions.method === 'DELETE' || response.status === 204) {
        return response.text();
      }
      return response.json();
    });
}

用户 token 在后端验证失败时的实现思路:

  • 后端的实现:对前端请求携带的 token 进行验证,如果无效时,直接响应401状态码。
  • 前端的实现:在 入口文件 ~/src/index.js 进行全局异常捕捉,把 ~/src/utils/request.js 里的 notification 提醒移到 ~/src/index.js
import 'babel-polyfill';
import dva from 'dva';
import { routerRedux } from 'dva/router';
import { notification } from 'antd';
import 'moment/locale/zh-cn';
import './g2';
import './rollbar';
// import browserHistory from 'history/createBrowserHistory';
import './index.less';

// 1. Initialize
const app = dva({
  onError(err, dispatch) {
    const { response, message } = err;
    const { status, url } = response;
    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: message,
    });
    if (status === 401) {
      dispatch(routerRedux.push('/user/login'));
    }
  },
  // history: browserHistory(),
});

// 2. Plugins
// app.use({});

// 3. Register global model
app.model(require('./models/global'));

// 4. Router
app.router(require('./router'));

// 5. Start
app.start('#root');

@chenshuai2144 #500 RP 对于 401 的处理应该跳转到 登录界面比较合理吧

@lawrence-peng 可以自己修改... 我们处理的是403 401 你可以自己处理

为了这个401跟服务端老哥撕逼死了好久他才愿意加😂

@lawrence-peng 请教一下,如果希望做登录保持的话,有什么比较好的处理方案呢?我这里所提的登录保持是指在token过期之后,自动使用refresh token去获取一个新的token,从而确保用户不会有体验上的问题。

我们现在的处理方案是登录之后执行setTimeout在token过期之前去服务端刷新token,不知道有没有更好的办法?

我建议是,重刷的逻辑交到后台的同学去实现。 获得两个token 向前端只返回 access_token,把 refresh_token 放到后端缓存中,并设置过期时间等于 refresh_token 的过期时间,后端验证到 access_token 过期,主动用 refresh_token 去换新的 access_token ,如果最后连 refresh_token 过期的话,就要求重新登录了。

@lawrence-peng request.js 里直接写了 headers 的状态有问题吧,应该是:

export default function request(url, options) {
  const defaultOptions = {
    credentials: 'include',
  };
  const newOptions = { ...defaultOptions, ...options };
  if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
    if (!(newOptions.body instanceof FormData)) {
      newOptions.headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        ...newOptions.headers,
      };
      newOptions.body = JSON.stringify(newOptions.body);
    } else {
      // newOptions.body is FormData
      newOptions.headers = {
        Accept: 'application/json',
        ...newOptions.headers,
      };
    }
  }
  newOptions.headers = {
    'Authorization': buildAuthorization(),
    ...newOptions.headers,
  };

谢谢你的建议,这样处理的话,后端就得做有状态的服务或者说用这个access token作为缓存的key来映射这个refresh token

@chenshuai2144 #500 RP 对于 401 的处理应该跳转到 登录界面比较合理吧

关闭页面时

我的一些思路:

用户 token 管理思路:

  • ~/src/utils/ 增加 token.js
  • ~/src/models/login.js login 方法里,把后端返回的 token 通过 token.js 里的方法 save 到 sessionStorage。(注:用 sessionStorage 好处是用户关闭页面时,sessionStorage 的值也随着删除)
  • ~/src/utils/request.js 发请求时,通过 header 头把 token 携带到后端进行验证
login.js

import { routerRedux } from 'dva/router';
import { fakeAccountLogin } from '../services/api';
import token from '../utils/token';

export default {
  namespace: 'login',

  state: {
    status: undefined,
  },

  effects: {
    *login({ payload }, { call, put }) {
      yield put({
        type: 'changeSubmitting',
        payload: true,
      });
      const response = yield call(fakeAccountLogin, payload);
      yield put({
        type: 'changeLoginStatus',
        payload: response,
      });
      // Login successfully
      token.save(response.token);

      if (response.status === 'ok') {
        yield put(routerRedux.push('/'));
      }
    },
    *logout(_, { put }) {

      // remove token in sessionStorage
      token.remove()

      yield put({
        type: 'changeLoginStatus',
        payload: {
          status: false,
        },
      });
      yield put(routerRedux.push('/user/login'));
    },
  },

  reducers: {
    changeLoginStatus(state, { payload }) {
      return {
        ...state,
        status: payload.status,
        type: payload.type,
        submitting: false,
      };
    },
    changeSubmitting(state, { payload }) {
      return {
        ...state,
        submitting: payload,
      };
    },
  },
};
token.js

import atob from 'atob';
import _ from 'lodash';

const STORAGE_TOKEN_NAME = 'TOKEN';

/**
 * JWT的方案
 */
export default {
  parse() {
    let token = this.get();
    try {
      const arr = token.split('.');
      if (arr.length === 3) {
        token = atob(token.split('.')[1]);
      }
      return JSON.parse(token);
    } catch (ex) {
      throw ex;
    }
  },
  check() {
    try {
      const payload = this.parse();
      return !_.isEmpty(payload);
    } catch (ex) {
      this.remove();
      return false;
    }
  },
  get() {
    return sessionStorage.getItem(STORAGE_TOKEN_NAME);
  },
  save(token) {
    sessionStorage.setItem(STORAGE_TOKEN_NAME, token);
  },
  remove() {
    sessionStorage.removeItem(STORAGE_TOKEN_NAME);
  },
};
import fetch from 'dva/fetch';
import token from './token';

const codeMessage = {
  200: '服务器成功返回请求的数据',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据,的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器',
  502: '网关错误',
  503: '服务不可用,服务器暂时过载或维护',
  504: '网关超时',
};


function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const errortext = codeMessage[response.status] || response.statusText;
  const error = new Error(errortext);
  error.response = response;
  throw error;
}

function buildAuthorization  ()  {
  const tokenVal = token.get();
  return (token !== '') ? `Bearer ${tokenVal}` : '';
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
 */
export default function request(url, options) {
  const defaultOptions = {
    credentials: 'include',
  };
  const newOptions = { ...defaultOptions, ...options };
  if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json; charset=utf-8',
      ...newOptions.headers,
    };
    newOptions.headers.Authorization = buildAuthorization(); // 增加的代码
    newOptions.body = JSON.stringify(newOptions.body);
  }

  return fetch(url, newOptions)
    .then(checkStatus)
    .then((response) => {
      if (newOptions.method === 'DELETE' || response.status === 204) {
        return response.text();
      }
      return response.json();
    });
}

用户 token 在后端验证失败时的实现思路:

  • 后端的实现:对前端请求携带的 token 进行验证,如果无效时,直接响应401状态码。
  • 前端的实现:在 入口文件 ~/src/index.js 进行全局异常捕捉,把 ~/src/utils/request.js 里的 notification 提醒移到 ~/src/index.js
import 'babel-polyfill';
import dva from 'dva';
import { routerRedux } from 'dva/router';
import { notification } from 'antd';
import 'moment/locale/zh-cn';
import './g2';
import './rollbar';
// import browserHistory from 'history/createBrowserHistory';
import './index.less';

// 1. Initialize
const app = dva({
  onError(err, dispatch) {
    const { response, message } = err;
    const { status, url } = response;
    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: message,
    });
    if (status === 401) {
      dispatch(routerRedux.push('/user/login'));
    }
  },
  // history: browserHistory(),
});

// 2. Plugins
// app.use({});

// 3. Register global model
app.model(require('./models/global'));

// 4. Router
app.router(require('./router'));

// 5. Start
app.start('#root');

关闭页面时? 应该是关闭浏览器时吧

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gaoqiang19514 picture gaoqiang19514  ·  3Comments

yaoleiroyal picture yaoleiroyal  ·  3Comments

suifan picture suifan  ·  3Comments

skyFi picture skyFi  ·  3Comments

renyi818 picture renyi818  ·  3Comments