簡體   English   中英

就無狀態前端客戶端而言,這種 JWT 邏輯有多安全?

[英]In terms of a stateless front-end client, how secure is this JWT logic?

我認為這個問題的目的並不重要我的確切設置是什么,但我只是在我的 React 和 React Native 應用程序中注意到了這一點,突然意識到他們實際上並沒有檢查存儲的 JWT 的任何類型的有效性.

這是代碼:

const tokenOnLoad = localStorage.getItem('token')

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

這可能不是真正的問題,因為令牌附加到標頭並且服務器將忽略沒有有效令牌的任何請求,但是有沒有辦法我可以將其升級為更好(即:更安全&加載UI的機會更少由於格式錯誤的令牌或如果有人入侵了他們自己的'token'而引爆)?

這是附加到每個請求的令牌:

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 和時間?

我只是想看看是否有任何低成本的方法來進一步驗證令牌並強化應用程序。

我也使用了一個requireAuth()高階組件,如果用戶沒有登錄,它將把用戶localStorage.setItem('token', 'lkjashkjdf') 。我覺得如果應用程序以某種方式做了localStorage.setItem('token', 'lkjashkjdf')我覺得可能會有一些糟糕的用戶體驗。

您的解決方案不是最佳的,因為您說您並沒有真正檢查用戶令牌的有效性。

讓我詳細說明您如何處理它:

1.在開始時間檢查令牌

  1. 等待redux-persistProvider組件中完成加載和注入
  2. 將 Login 組件設置為所有其他組件的父級
  3. 檢查令牌是否仍然有效 3.1。 是:顯示孩子 3.2。 否:顯示登錄表單

2. 當用戶當前正在使用應用程序時

您應該利用中間件的力量並在用戶進行的每次dispatch檢查令牌的有效性。

如果令牌已過期,則調度操作以使令牌無效。 否則,就好像什么都沒發生一樣繼續。

看看下面的中間件token.js


我寫了一個完整的代碼示例供您使用並在需要時進行調整。

我在下面提出的解決方案與路由器無關。 如果您使用react-router以及任何其他路由器,則可以使用它。

應用入口點:app.js

看到Login組件在路由器的頂部

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>
    );
  }
}

商店.js

這里要記住的關鍵是使用redux-persist並將 login reducer 保存在本地存儲(或任何存儲)中。

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

這是一個要添加的中間件,以檢查令牌是否仍然有效。

如果令牌不再有效,則觸發調度以使其無效。

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)

如果登錄數據沒有失效,則表示用戶已連接並且令牌仍然有效。 (否則它會被中間件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;
  }
}

隨時問我任何問題,或者如果您需要我解釋更多有關哲學的信息。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM