对于无状态的前端客户端,这个 JWT 逻辑有多安全?

Posted

技术标签:

【中文标题】对于无状态的前端客户端,这个 JWT 逻辑有多安全?【英文标题】:In terms of a stateless front-end client, how secure is this JWT logic? 【发布时间】:2018-04-14 06:26:12 【问题描述】:

对于这个问题,我认为我的确切设置是什么并不重要,但我只是在我的 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'),可能会有一些糟糕的用户体验。

【问题讨论】:

【参考方案1】:

您的解决方案不是最优的,因为您说您并没有真正检查用户令牌的有效性。

让我详细说明如何处理它:

1.在开始时检查令牌

    等待redux-persist 完成加载和注入Provider 组件 将 Login 组件设置为所有其他组件的父组件 检查令牌是否仍然有效 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>
    );
  

store.js

这里要记住的关键是使用redux-persist 并将登录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仍然有效。 (否则会被中间件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;
  

如果您需要我解释更多关于哲学的问题,请随时问我。

【讨论】:

这很棒。感谢您发布它。我需要一些时间来全面分析它,但听起来我应该介绍一个应用程序正在初始化的状态,我已经有了动作创建者。很像使用 null 进行初始化并且在值移动到 truefalse 之前不信任状态。 我绝对需要一个步骤来检查令牌的到期情况。这就是我可能非常有效地使其无效的地方。 为了准确起见,我们使用 Redis 来维护其各自用户的有效令牌列表。服务器维护用户白名单,而不是被禁止用户的黑名单。这样我们就可以很容易地限制流量。 您的示例代码架构非常强大。我认为你的根级登录组件做得很好。 我可能会在完成后以另一个答案的形式更新我的问题,因为它相当复杂,应该展示给其他人以供采样。本质上,我需要做的就是添加几行代码来检查 JWT 并使用结果更新 redux 中的isInvalidated,这将被输入到 root auth 组件中。

以上是关于对于无状态的前端客户端,这个 JWT 逻辑有多安全?的主要内容,如果未能解决你的问题,请参考以下文章

spring security jwt 安全校验

Springboot 整合jwt

无法理解 JWT 和 Java (Spring Boot)。这个应用程序安全吗?

新的无状态 JWT 认证理念!它真的安全吗?

安全认证--JWT介绍及使用

jwt入门