对于无状态前端客户端,此JWT逻辑的安全性如何? [英] In terms of a stateless front-end client, how secure is this JWT logic?

查看:53
本文介绍了对于无状态前端客户端,此JWT逻辑的安全性如何?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

对于这个问题,我认为我的确切设置是什么并不重要,但是我只是在我的React和React Native应用程序中注意到了这一点,突然意识到他们实际上并没有检查JWT的任何形式的有效性

I don't think it matters for the purpose of this question what my exact setup is, but I just noticed this in my React and React Native apps, and suddenly realized they are not actually checking any kind of validity of the JWT that is stored.

这是代码:

const tokenOnLoad = localStorage.getItem('token')

if (tokenOnLoad) store.dispatch({ type: AUTH_USER })

这可能不是真正的问题,因为令牌已附加到标头,并且服务器将在没有有效令牌的情况下忽略任何请求,但是有办法我可以将其升级为更好的版本(即:更安全&& ;是否会减少加载因令牌格式错误或有人在自己的'token'中被黑客入侵而引爆的UI的机会?

It's probably not really an issue, because the token is attached to the headers and the server will ignore any request without a valid token, but is there a way I can upgrade this to be better (ie: more secure && less chance of loading UI that detonates due to malformed token or if someone hacked in their own 'token') ?

以下是令牌附加到每个请求上的信息:

Here is the token getting attached to every request:

networkInterface.use([{
  applyMiddleware(req, next) {
    if (!req.options.headers) req.options.headers = {}
    const token = localStorage.getItem('token')
    req.options.headers.authorization = token || null
    next()
  }
}])

我是否应该添加一些逻辑以至少检查令牌的长度或对其进行解码,并检查其中是否包含用户ID?还是在服务器运行时浪费CPU和时间?

Should I add some logic to at least check the length of the token or decode it and check if it has a user id in it? Or, is that a waste of CPU and time when the server does it?

我只是想看看是否有任何低成本的方法来进一步验证令牌并强化应用程序.

I'm just looking to see if there are any low-cost ways to further validate the token and harden the app.

我还使用了requireAuth()个高阶组件,如果用户未登录则将其踢出.我觉得,如果该应用程序以某种方式localStorage.setItem('token', 'lkjashkjdf')进行操作,可能会出现一些不良的UX.

I do also use a requireAuth() higher-order component that kicks users out if they are not logged in. I feel like there could be some bad UX if the app somehow did localStorage.setItem('token', 'lkjashkjdf').

推荐答案

您的解决方案并不是最佳选择,因为您声明自己并未真正检查用户令牌的有效性.

Your solution is not optimal as you stated you don't really check the validity of the user's token.

让我详细说明如何处理:

Let me detail how you can handle it:

1.在开始时检查令牌

  1. 等待redux-persist完成对Provider组件的加载和注入
  2. 将登录"组件设置为所有其他组件的父组件
  3. 检查令牌是否仍然有效 3.1.是:向孩子们展示 3.2.否:显示登录表单
  1. Wait for the redux-persist to finish loading and injecting in the Provider component
  2. Set the Login component as the parent of all the other components
  3. Check if the token is still valid 3.1. Yes: Display the children 3.2. No: Display the login form

2.当用户当前正在使用该应用程序时

您应该使用中间件的功能,并检查用户制作的每个dispatch中的令牌有效性.

You should use the power of middlewares and check the token validity in every dispatch the user makes.

如果令牌已过期,请分派一个使令牌无效的操作.否则,请继续进行,好像什么都没发生.

If the token is expired, dispatch an action to invalidate the token. Otherwise, continue as if nothing happened.

看看下面的中间件token.js.

我编写了完整的代码示例,供您使用,并在需要时进行修改.

I wrote a whole sample of code for your to use and adapt it if needed.

我在下面提出的解决方案与路由器无关. 如果您使用react-router,也可以将其与其他任何路由器一起使用.

The solution I propose below is router agnostic. You can use it if you use react-router but also with any other router.

应用入口点:app.js

请确保Login组件位于路由器顶部

See that the Login component is on top of the routers

import React from 'react';

import { Provider } from 'react-redux';
import { browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

import createRoutes from './routes'; // Contains the routes
import { initStore, persistReduxStore } from './store';
import { appExample } from './container/reducers';

import Login from './views/login';

const store = initStore(appExample);

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { rehydrated: false };
  }

  componentWillMount() {
    persistReduxStore(store)(() => this.setState({ rehydrated: true }));
  }

  render() {
    const history = syncHistoryWithStore(browserHistory, store);
    return (
      <Provider store={store}>
        <Login>
          {createRoutes(history)}
        </Login>
      </Provider>
    );
  }
}

store.js

这里要记住的关键是使用redux-persist并将登录还原程序保存在本地存储(或其他存储)中.

The key to remember here is to use redux-persist and keep the login reducer in the local storage (or whatever storage).

import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { persistStore, autoRehydrate } from 'redux-persist';
import localForage from 'localforage';
import { routerReducer } from 'react-router-redux';

import reducers from './container/reducers';
import middlewares from './middlewares';

const reducer = combineReducers({
  ...reducers,
  routing: routerReducer,
});

export const initStore = (state) => {
  const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const store = createStore(
    reducer,
    {},
    composeEnhancers(
      applyMiddleware(...middlewares),
      autoRehydrate(),
    ),
  );

  persistStore(store, {
    storage: localForage,
    whitelist: ['login'],
  });

  return store;
};

export const persistReduxStore = store => (callback) => {
  return persistStore(store, {
    storage: localForage,
    whitelist: ['login'],
  }, callback);
};

中间件:token.js

这是要添加的中间件,以检查令牌是否仍然有效.

This is a middleware to add in order to check wether the token is still valid.

如果令牌不再有效,则会触发分派以使令牌无效.

If the token is no longer valid, a dispatch is trigger to invalidate it.

import jwtDecode from 'jwt-decode';
import isAfter from 'date-fns/is_after';

import * as actions from '../container/actions';

export default function checkToken({ dispatch, getState }) {
  return next => (action) => {
    const login = getState().login;

    if (!login.isInvalidated) {
      const exp = new Date(jwtDecode(login.token).exp * 1000);
      if (isAfter(new Date(), exp)) {
        setTimeout(() => dispatch(actions.invalidateToken()), 0);
      }
    }

    return next(action);
  };
}

登录组件

这里最重要的是对 if (!login.isInvalidated) 的测试.

The most important thing here is the test of if (!login.isInvalidated).

如果登录数据未失效,则表示用户已连接并且令牌仍然有效. (否则它将被中间件token.js无效)

If the login data is not invalidated, it means that the user is connected and the token is still valid. (Otherwise it would have been invalidated with the middleware token.js)

import React from 'react';
import { connect } from 'react-redux';

import * as actions from '../../container/actions';

const Login = (props) => {
  const {
    dispatch,
    login,
    children,
  } = props;

  if (!login.isInvalidated) {
    return <div>children</div>;
  }

  return (
    <form onSubmit={(event) => {
      dispatch(actions.submitLogin(login.values));
      event.preventDefault();
    }}>
      <input
        value={login.values.email}
        onChange={event => dispatch({ type: 'setLoginValues', values: { email: event.target.value } })}
      />
      <input
        value={login.values.password}
        onChange={event => dispatch({ type: 'setLoginValues', values: { password: event.target.value } })}
      />
      <button>Login</button>
    </form>
  );
};

const mapStateToProps = (reducers) => {
  return {
    login: reducers.login,
  };
};

export default connect(mapStateToProps)(Login);

登录操作

export function submitLogin(values) {
  return (dispatch, getState) => {
    dispatch({ type: 'readLogin' });
    return fetch({}) // !!! Call your API with the login & password !!!
      .then((result) => {
        dispatch(setToken(result));
        setUserToken(result.token);
      })
      .catch(error => dispatch(addLoginError(error)));
  };
}

export function setToken(result) {
  return {
    type: 'setToken',
    ...result,
  };
}

export function addLoginError(error) {
  return {
    type: 'addLoginError',
    error,
  };
}

export function setLoginValues(values) {
  return {
    type: 'setLoginValues',
    values,
  };
}

export function setLoginErrors(errors) {
  return {
    type: 'setLoginErrors',
    errors,
  };
}

export function invalidateToken() {
  return {
    type: 'invalidateToken',
  };
}

减少登录次数

import { combineReducers } from 'redux';
import assign from 'lodash/assign';
import jwtDecode from 'jwt-decode';

export default combineReducers({
  isInvalidated,
  isFetching,
  token,
  tokenExpires,
  userId,
  values,
  errors,
});

function isInvalidated(state = true, action) {
  switch (action.type) {
    case 'readLogin':
    case 'invalidateToken':
      return true;
    case 'setToken':
      return false;
    default:
      return state;
  }
}

function isFetching(state = false, action) {
  switch (action.type) {
    case 'readLogin':
      return true;
    case 'setToken':
      return false;
    default:
      return state;
  }
}

export function values(state = {}, action) {
  switch (action.type) {
    case 'resetLoginValues':
    case 'invalidateToken':
      return {};
    case 'setLoginValues':
      return assign({}, state, action.values);
    default:
      return state;
  }
}

export function token(state = null, action) {
  switch (action.type) {
    case 'invalidateToken':
      return null;
    case 'setToken':
      return action.token;
    default:
      return state;
  }
}

export function userId(state = null, action) {
  switch (action.type) {
    case 'invalidateToken':
      return null;
    case 'setToken': {
      const { user_id } = jwtDecode(action.token);
      return user_id;
    }
    default:
      return state;
  }
}

export function tokenExpires(state = null, action) {
  switch (action.type) {
    case 'invalidateToken':
      return null;
    case 'setToken':
      return action.expire;
    default:
      return state;
  }
}

export function errors(state = [], action) {
  switch (action.type) {
    case 'addLoginError':
      return [
        ...state,
        action.error,
      ];
    case 'setToken':
      return state.length > 0 ? [] : state;
    default:
      return state;
  }
}

可以随时问我任何问题,或者是否需要我对哲学进行更多解释.

Feel free to ask me any question or if you need me to explain more on the philosophy.

这篇关于对于无状态前端客户端,此JWT逻辑的安全性如何?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆