React 身份验证会话管理

Posted

技术标签:

【中文标题】React 身份验证会话管理【英文标题】:React Authentication session management 【发布时间】:2019-12-28 20:50:44 【问题描述】:

我正在开发这个由两部分组成的应用程序(React + Express... + Apollo(用于 GraphQL))。 为了管理身份验证,我一直在阅读指南和观看视频,并使用JWT token 和Context API 取得了一些进展,例如:

    每当用户登录时,React 都会使用 gql(由 apollo-boost 提供支持)向 Express 发出请求。 Express 服务器(后端)向其发送响应,然后使用解析器处理其数据:
require('dotenv').config();
import  User  from '../models/User';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

export default 
  Query: 
    login: async (_,  email, password , req) => 
      try 
        const user = await User.findOne( email );
        if (!user) 
          throw new Error('User don\'t exist');
        

        const isEqual = await bcrypt.compare(password, user.password);
        if (!isEqual) 
          throw new Error('Wrong credentials');
        

        const 
          JWTSECRET = 'secret'
         = process.env;

        const token = jwt.sign( userId: user.id, email: user.email , JWTSECRET, 
          expiresIn: '1h'
        );

        return 
          userId: user.id,
          token,
          tokenExpiration: 3600000 // Date.now()+3600000 doesn't work cause Int is 32-bit signed
        

       catch (e) 
        throw new Error(e.message);
      
    
  
;

const hashPassword = async password => 
  const saltRounds = 10;

  const hashedPassword = await new Promise((resolve, reject) => 
    try 
      bcrypt.hash(password, saltRounds, (err, hash) => 
        if (err) reject(err);
        resolve(hash);
      );
     catch (e) 
      throw new Error(e.message);
    
  );

  return hashedPassword;
;
    React 获取响应并将其附加到上下文中,我猜它还会分配一些 localStorage 的东西来保持会话,例如:
import React from 'react';
import AuthContext from './auth-context';
import ApolloClient,  gql  from 'apollo-boost';

function getClient() 
    return new ApolloClient( uri: 'http://localhost:4000/ws', credentials: 'same-origin' );


const AuthState = props => 
    const defaultValue = 
        userId: null,
        userFirstName: null,
        userLastName: null,
        userEmail: null,
        token: null,
        tokenExpiration: null,
        createdOn: null,
        errors: [],
        login,
        get,
    ;

    return (
        <AuthContext.Provider value=defaultValue>
            props.children
        </AuthContext.Provider>
    );

    /**
     * Login
     * @param string email 
     * @param string password 
     */
    async function login(email, password) 
        const client = getClient();
        const CHECK_CREDENTIALS = gql`
            query 
                login(email: "$email", password: "$password") 
                    userId
                    token
                    tokenExpiration
                
            
        `;
        const  data, loading, errors  = await client.query(
            query: CHECK_CREDENTIALS,
            errorPolicy: 'all'
        );

        if (loading) 
            defaultValue.message = "Validating credentials";
        

        if (errors) 
            defaultValue.errors = errors.map(error => <p>error.message</p>);
        

        if (data && data.login) 
            defaultValue.userId = data.login.userId;
            defaultValue.token = data.login.token;
            defaultValue.tokenExpiration = data.login.tokenExpiration;
            // Persist session ?
            localStorage.setItem('user-token', data.login.token);
            localStorage.setItem('user-token-expiration', data.login.tokenExpiration);
        
    

    async function get(userId) 
        const client = getClient();
        const GET_USER = gql`
            query 
                user(_id: "$userId") 
                    firstName
                    lastName
                    email
                
            
        `;

        const  data, loading, errors  = await client.query(
            query: GET_USER,
            errorPolicy: 'all'
        );

        if (loading) 
            defaultValue.message = "Getting user";
        

        if (errors) 
            defaultValue.errors = errors.map(error => <p>error.message</p>);
        

        if (data && data.user) 
            defaultValue.userFirstName = data.user.firstName;
            defaultValue.userLastName = data.user.lastName;
            defaultValue.userEmail = data.user.email;
        
    


export default AuthState;
    在每个需要身份验证的组件中,我都会检查 context.tokencontext.tokenExpiration 并重定向到登录或让它们通过。

我一直想知道如果我使用开发者控制台手动设置一些user-tokenuser-token-expiration 会怎样?我错过了什么?

感谢任何cmets。

【问题讨论】:

你的代码有效吗? @Peter,我还没有尝试使用localStorage 管理,但如果不使用它,它会在我重新加载页面之前工作。 【参考方案1】:

嗯...我一直在研究代码,并验证了 login 解析器使用秘密签名来验证令牌。

login: async (_,  email, password , req) => 
  try 
    const user = await User.findOne( email );
    if (!user) 
      throw new Error('wrong credentials');
    

    const isEqual = await bcrypt.compare(password, user.password);
    if (!isEqual) 
      throw new Error('wrong credentials');
    

    const 
      JWTSECRET = 'secret'
     = process.env;

    const token = jwt.sign( userId: user.id, email: user.email , JWTSECRET, 
      expiresIn: '1h'
    ); 

    return 
      userId: user.id,
      token,
      tokenExpiration: Date.now() + 3600000
    

   catch (e) 
    throw new Error(e.message);
  

为了看到实际情况,在生成令牌后转到JWT.IO Official page 并将其粘贴到那里。然后在 Verify signature 字段中插入“秘密”或任何内容,它会将 Invalid signature 更改为 Verified

【讨论】:

以上是关于React 身份验证会话管理的主要内容,如果未能解决你的问题,请参考以下文章

使用 Express 会话反应本机身份验证?

会话管理:如何为 REST 服务生成身份验证令牌? (球衣)

如何使用 MongoDB + NodeJS Express 向 ReactJS React Router 和 Redux 添加登录身份验证和会话?

iOS 中的身份验证和会话管理

具有基于 HttpOnly cookie 的身份验证和会话管理的单页应用程序

保持 React 基于 Cookie 的身份验证状态