PrivateOutlet.js 身份验证后不呈现页面

PrivateOutlet.js doesn't render page after authentication

当我尝试下面的代码时,我被重定向到登录页面,就好像我没有通过身份验证一样。登录后我无法查看关于页面,因为它会将我定向到欢迎页面,因为登录页面中的逻辑(如果 isAuthenticated 导航到欢迎页面)。如果我删除登录页面中的逻辑,我只会卡在登录页面中。为什么我无法查看关于页面?

PrivateOutlet.js;

import React from 'react';
import { Outlet, Navigate } from 'react-router-dom';

const PrivateOutlet = ({ isAuthenticated }) => {
  if (isAuthenticated ) { 
    return <Outlet /> 
  } else {
    return <Navigate to='login' /> //Go to login
  }
};

export default PrivateOutlet;

已更新 PrivateOutlet.js ;

import React from 'react';
import { connect } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';

const PrivateOutlet = ({ isAuthenticated }) => {
  return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
};
const mapStateToProps = state => ({
    isAuthenticated: state.auth.isAuthenticated
});

export default connect(mapStateToProps)(PrivateOutlet);

App.js

function App() {
  return (
    <Provider store={store}>
      <Router>
        <Layout>
          <Routes>
            <Route path='/' element={<WelcomePage/>} />
            <Route path='/home' element={<Home/>} />

            <Route element={<PrivateOutlet/>}>
              <Route path='/about' element={<About/>} />
            </Route>

            <Route path='/contact' element={<Contact/>} />
            <Route path='/login' element={<Login/>} />
            <Route path='/signup' element={<Signup/>} />
            <Route path='/reset-password' element={<ResetPassword/>} />
            <Route path='/password/reset/confirm/:uid/:token' element={<ResetPasswordConfirm/>} />
            <Route path='/activate/:uid/:token' element={<Activate/>} />
            <Route path='*' element={<NotFound/>} />
          </Routes>
        </Layout>
      </Router>
    </Provider>
  );
}

export default App;

login.js

import React, { useState } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { connect } from 'react-redux';
import { Button } from '@mui/material';
import { login } from '../actions/auth';
import './Login.css';
import { Helmet } from 'react-helmet';

function Login({ login, isAuthenticated }) {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });

  const { email, password } = formData;
  const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value});
  const onSubmit = e => {
    e.preventDefault();

    login (email, password)
  };
 
  if (isAuthenticated) {
    return <Navigate to='/' />
  }

  return (
    <div className='login'>
      <Helmet>
        <title>Prosperity - Login</title>
        <meta
          name='description'
          content='login page'
        />
      </Helmet>
      <h1 className='login__title'>Login</h1>
      <p className='login__lead'>Login into your Account</p>
      <form className='login__form' onSubmit={e => onSubmit(e)}>
        <div className='login__form__group'>
          <input
            className='login__form__input'
            type='email'
            placeholder='Email *'
            name='email'
            value={email}
            onChange={e => onChange(e)}
            required
          />
        </div>
        <div className='login__form__group'>
          <input
            className='login__form__input'
            type='password'
            placeholder='Password *'
            name='password'
            value={password}
            onChange={e => onChange(e)}
            minLength='8'
            required
          />
        </div>
        <Button className='login__button__main' type='submit'>Login</Button>
      </form>
      <p className='link__to__Signup'>
        Do not have an account? <Link to='/signup' className='login__link'>Register</Link>
      </p>
      <p className='link__to__resetPassword'>
        Forgot Password? <Link to='/reset-password' className='reset__password__link'>Reset Password</Link>
      </p>

    </div>
  )
};
const mapStateToProps = state => ({
  isAuthenticated: state.auth.isAuthenticated
});

export default connect (mapStateToProps, { login }) (Login);

actions/Auth.js ;

import axios from 'axios';
import { setAlert } from './alert';
import {
    LOGIN_SUCCESS,
    LOGIN_FAIL,
    SIGNUP_SUCCESS,
    SIGNUP_FAIL,
    ACTIVATION_SUCCESS,
    ACTIVATION_FAIL,
    USER_LOADED_SUCCESS,
    USER_LOADED_FAIL,
    AUTHENTICATED_SUCCESS,
    AUTHENTICATED_FAIL,
    PASSWORD_RESET_SUCCESS,
    PASSWORD_RESET_FAIL,
    PASSWORD_RESET_CONFIRM_SUCCESS,
    PASSWORD_RESET_CONFIRM_FAIL,
    LOGOUT
} from './types';

export const checkAuthenticated = () => async dispatch => {
    if (localStorage.getItem('access')) {
        const config = {
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            }
        };

        const body = JSON.stringify({ token: localStorage.getItem('access') });

        try {
            const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/jwt/verify/`, body, config)

            if (res.data.code !== 'token_not_valid') {
                dispatch({
                    type: AUTHENTICATED_SUCCESS
                });
            } else {
                dispatch({
                    type: AUTHENTICATED_FAIL
                });
            }
        } catch (err) {
            dispatch({
                type: AUTHENTICATED_FAIL
            });
        }

    } else {
        dispatch({
            type: AUTHENTICATED_FAIL
        });
    }
};

export const load_user = () => async dispatch => {
    if (localStorage.getItem('access')) {
        const config = {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `JWT ${localStorage.getItem('access')}`,
                'Accept': 'application/json'
            }
        }; 
        
        try {
            const res = await axios.get(`${process.env.REACT_APP_API_URL}/auth/users/me/`, config);
            dispatch({
                type: USER_LOADED_SUCCESS,
                payload: res.data
            });
        }catch (err) {
            dispatch({
                type: USER_LOADED_FAIL
            });
        }
    } else {
        dispatch({
            type: USER_LOADED_FAIL
        });
    }
};

export const login = (email, password) => async dispatch => {
    const config = {
        headers: {
            'Content-Type': 'application/json'
        }
    };

    const body = JSON.stringify({ email, password });

    try {
            const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/jwt/create/`, body, config);
            dispatch({
                type: LOGIN_SUCCESS,
                payload: res.data
            });
            dispatch(setAlert('Authenticated successfully', 'success'));
            dispatch(load_user());
        }catch (err) {
            dispatch({
                type: LOGIN_FAIL
            });
            dispatch(setAlert('Error Authenticating', 'error'));
    }
};

export const signup = (name, email, password, re_password) => async dispatch => {
    const config = {
        headers: {
            'Content-Type': 'application/json'
        }
    };

    const body = JSON.stringify({ name, email, password, re_password });

    try {
        const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/users/`, body, config);

        dispatch({
            type: SIGNUP_SUCCESS,
            payload: res.data
        });
        dispatch(setAlert('Check Your Email to Activate Your Account.', 'warning'));
    } catch (err) {
        dispatch({
            type: SIGNUP_FAIL
        })
    }
};

export const verify = (uid, token) => async dispatch => {
    const config = {
        headers: {
            'Content-Type': 'application/json'
        }
    };

    const body = JSON.stringify({ uid, token });

    try {
        await axios.post(`${process.env.REACT_APP_API_URL}/auth/users/activation/`, body, config);

        dispatch({
            type: ACTIVATION_SUCCESS,
        });
        dispatch(setAlert('Account Activated Successfully.', 'success'));
    } catch (err) {
        dispatch({
            type: ACTIVATION_FAIL
        })
    }
};

//Reset Password
export const reset_password = (email) => async dispatch => {
    const config = {
        headers: {
            'Content-Type': 'application/json'
        }
    };

    const body = JSON.stringify({ email });
    try {
        await axios.post (`${process.env.REACT_APP_API_URL}/auth/users/reset_password/`, body, config);
        dispatch({
            type: PASSWORD_RESET_SUCCESS
        });
        dispatch(setAlert('Check Your Email to Rest Password.', 'warning'));
    } catch (err) {
        dispatch({
            type: PASSWORD_RESET_FAIL
        });
    }
};

// Reset Password Confirm
export const reset_password_confirm = (uid, token, new_password, re_new_password) => async dispatch => {
    const config = {
        headers: {
            'Content-Type': 'application/json'
        }
    };

    const body = JSON.stringify({ uid, token, new_password, re_new_password });
    
    try {
        await axios.post (`${process.env.REACT_APP_API_URL}/auth/users/reset_password_confirm/`, body, config);
        dispatch(setAlert('Password Rest Successful.', 'success'));
        dispatch({
            type: PASSWORD_RESET_CONFIRM_SUCCESS
        });
    } catch (err) {
        dispatch({
            type: PASSWORD_RESET_CONFIRM_FAIL
        });
    }
};

//Logout
export const logout = () => dispatch => {
    dispatch(setAlert('Logout successful.', 'success'));
    dispatch({
        type: LOGOUT
    });
};

reducers/Auth.js ;

import {
    LOGIN_SUCCESS,
    LOGIN_FAIL,
    SIGNUP_SUCCESS,
    SIGNUP_FAIL,
    ACTIVATION_SUCCESS,
    ACTIVATION_FAIL,
    USER_LOADED_SUCCESS,
    USER_LOADED_FAIL,
    AUTHENTICATED_SUCCESS,
    AUTHENTICATED_FAIL,
    PASSWORD_RESET_SUCCESS,
    PASSWORD_RESET_FAIL,
    PASSWORD_RESET_CONFIRM_SUCCESS,
    PASSWORD_RESET_CONFIRM_FAIL,
    LOGOUT
} from '../actions/types';

const initialState = {
    access: localStorage.getItem('access'),
    refresh: localStorage.getItem('refresh'),
    isAuthenticated: null,
    user: null,
};

export default function (state = initialState, action) {
    const { type, payload } = action;

    switch(type) {
        case AUTHENTICATED_SUCCESS:
            return {
                ...state,
                isAuthenticated: true
            }
        case LOGIN_SUCCESS:
            localStorage.setItem('access', payload.access);
            localStorage.setItem('refresh', payload.refresh);
            return {
                ...state,
                isAuthenticated: true,
                access: payload.access,
                refresh: payload.refresh,
            }
        case USER_LOADED_SUCCESS:
            return {
                ...state,
                user: payload
            }
        case SIGNUP_SUCCESS:
            return {
                ...state,
                isAuthenticated: false,
            }
        case AUTHENTICATED_FAIL:
            return {
                ...state,
                isAuthenticated: false
            }
        case USER_LOADED_FAIL:
            return {
                ...state,
                user: null
            }
        case LOGIN_FAIL:
        case SIGNUP_FAIL:
        case LOGOUT:
            localStorage.removeItem('access');
            localStorage.removeItem('refresh');
            return {
                ...state,
                access: null,
                refresh: null,
                isAuthenticated: false,
                user: null,
            }
        case PASSWORD_RESET_SUCCESS:
        case PASSWORD_RESET_FAIL:
        case ACTIVATION_SUCCESS:
        case ACTIVATION_FAIL:
        case PASSWORD_RESET_CONFIRM_SUCCESS:
        case PASSWORD_RESET_CONFIRM_FAIL:
            return {
                ...state
            }

        default:
            return state
    }
};

isAuthenticated 未作为道具传递给 PrivateOutlet。

<Route element={<PrivateOutlet />}> // <-- no isAuthenticated prop
  <Route path='/about' element={<About />} />
</Route>

isAuthenticated存储在redux状态,初始值为null 而不是 true|false 在成功或失败的身份验证尝试之后。

const initialState = {
  access: localStorage.getItem('access'),
  refresh: localStorage.getItem('refresh'),
  isAuthenticated: null,
  user: null,
};

您可以明确检查 isAuthenticated 状态是否为 null 并且有条件地 return 为 null 或加载指示器等...同时正在解析身份验证状态。一旦身份验证状态解析为 non-null 值,则可以呈现路由组件或重定向。

import React from 'react';
import { connect } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';

const PrivateOutlet = ({ isAuthenticated }) => {
  if (isAuthenticated === null) {
    return null;
  }
  return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
};

const mapStateToProps = state => ({
  isAuthenticated: state => state.auth.isAuthenticated,
});

export default connect(mapStateToProps)(PrivateOutlet);

import React from 'react';
import { useSelector } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';

const PrivateOutlet = () => {
  const isAuthenticated = useSelector(state => state.auth.isAuthenticated);

  if (isAuthenticated === null) {
    return null;
  }
  return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
};

export default PrivateOutlet;

更新

如果您想将用户重定向回他们最初尝试访问的页面,PrivateOutlet 组件应该获取当前位置并将其在路由状态中传递到登录页面。

import { Outlet, Navigate, useLocation } from 'react-router-dom';

const PrivateOutlet = () => {
  const location = useLocation();
  const isAuthenticated = useSelector(state => state.auth.isAuthenticated);

  if (isAuthenticated === null) {
    return null;
  }
  return isAuthenticated
    ? <Outlet />
    : <Navigate to='/login' state={{ from: location }} replace />;
};

然后 Login 组件从路由状态中获取这个值以强制导航回原始路由。

const navigate = useNavigate();
const { state } = useLocation();
const { from } = state || {};

...

navigate(from.pathname || "/home", { state: from.state, replace: true });

例子

import React, { useState } from 'react';
import { Link, Navigate, useLocation } from 'react-router-dom';
...

function Login({ login, isAuthenticated }) {
  const { state } = useLocation();
  const { from } = state || {};

  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });

  const { email, password } = formData;

  const onChange = e => setFormData({
    ...formData,
    [e.target.name]: e.target.value
  });

  const onSubmit = e => {
    e.preventDefault();

    login(email, password);
  };
 
  if (isAuthenticated) {
    return (
      <Navigate
        to={from.pathname || "/home"}
        replace
        state={from.state}
      />
    );
  }

  return (
    ...
  );
};

const mapStateToProps = state => ({
  isAuthenticated: state.auth.isAuthenticated
});

更新 2

I get on the console; Login.js:30 Uncaught TypeError: Cannot read properties of undefined (reading 'pathname')

我认为这里发生的是 login 操作更新了你的 redux store,应该 触发组件重新渲染,我怀疑它是 this 重新呈现会丢失路由状态。路由状态非常短暂,仅在接收到它时存在于转换和渲染周期中。您可能会使用 React ref 来缓存路由状态的副本以备后用。

示例:

import React, { useRef, useState } from 'react';
import { Link, Navigate, useLocation } from 'react-router-dom';
...

function Login({ login, isAuthenticated }) {
  const { state } = useLocation();
  const { from } = state || {};

  const fromRef = useRef(from);

  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });

  const { email, password } = formData;

  const onChange = e => setFormData({
    ...formData,
    [e.target.name]: e.target.value
  });

  const onSubmit = e => {
    e.preventDefault();

    login(email, password);
  };
 
  if (isAuthenticated) {
    return (
      <Navigate
        to={fromRef.current.pathname || "/home"}
        replace
        state={fromRef.current.state}
      />
    );
  }

  return (
    ...
  );
};

将“onLoginSuccess”处理程序传递给 login 操作并从异步操作发出命令式导航可能更实用。

示例:

import React, { useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
...

function Login({ login, isAuthenticated }) {
  const navigate = useNavigate();
  const { state } = useLocation();
  const { from } = state || {};

  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });

  const { email, password } = formData;

  const onChange = e => setFormData({
    ...formData,
    [e.target.name]: e.target.value
  });

  const onSubmit = e => {
    e.preventDefault();

    const onSuccess = () => {
      navigate(
        from.pathname || "/home",
        {
          replace: true,
          state: from.state
        }
      );
    };

    login(email, password, onSuccess);
  };
 
  return (
    ...
  );
};

...

export const login = (email, password, onSuccess) => async dispatch => {
  const config = {
    headers: {
      'Content-Type': 'application/json'
    }
  };

  const body = JSON.stringify({ email, password });

  try {
    const res = await axios.post(
      `${process.env.REACT_APP_API_URL}/auth/jwt/create/`,
      body,
      config
    );
    dispatch({ type: LOGIN_SUCCESS, payload: res.data });
    dispatch(setAlert('Authenticated successfully', 'success'));
    dispatch(load_user());
    if (onSuccess) {
      onSuccess();
    }
  } catch (err) {
    dispatch({ type: LOGIN_FAIL });
    dispatch(setAlert('Error Authenticating', 'error'));
  }
};