如何在反应应用程序中令牌过期时注销用户
How to logout user when token expires in react app
我正在开发一个应用程序,我使用 React 作为我的 front-end 和 React-apollo-graphql
作为我的 API 调用。
我正在使用 react-hooks
即 React 16.8 +.
我在做什么
我创建了一个 auth.js
文件,当用户登录时我在其中存储我的值,并检查令牌是否有效(我正在检查过期),但该文件仅在加载我正在刷新或重新加载页面,这不是它应该的工作方式
我的auth.js文件
const initialstate = {
user: null,
};
if (localStorage.getItem("JWT_Token")) {
const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
console.log(jwt_Token_decoded.exp * 1000);
console.log(Date.now());
if (jwt_Token_decoded.exp * 1000 < Date.now()) {
localStorage.clear(); // this runs only when I refresh the page or reload on route change it dosent work
} else {
initialstate.user = jwt_Token_decoded;
}
}
const AuthContext = createContext({
user: null,
login: (userData) => {},
logout: () => {},
});
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return {
...state,
user: action.payload,
};
case "LOGOUT":
return {
...state,
user: null,
};
default:
return state;
}
};
const AuthProvider = (props) => {
const [state, dispatch] = useReducer(AuthReducer, initialstate);
const login = (userData) => {
localStorage.setItem("JWT_Token", userData.token);
dispatch({
type: "LOGIN",
payload: userData,
});
};
const logout = () => {
localStorage.clear();
dispatch({ action: "LOGOUT" });
};
return (
<AuthContext.Provider
value={{ user: state.user, login, logout }}
{...props}
/>
);
};
export { AuthContext, AuthProvider };
正如我在检查令牌到期的行中所注释的那样。
我唯一的问题是为什么它在页面重新加载上工作,而不是像我们在使用 Redux 时在存储文件中所做的那样在每个路由上工作。
我的App.js
<AuthProvider>
<Router>
<div className="App wrapper">
<Routes/>
</div>
</Router>
</AuthProvider>
我的index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from '@apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
const client = new ApolloClient({
uri: 'my url',
cache: new InMemoryCache(),
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
要点
因为我正在使用 react-apollo-graphql
所以他们提供 ant 身份验证流程吗?就像 redux 一样,我们必须创建一个存储文件来存储我们的数据
我使用的是 React 16.8 + 所以我使用的是 react-hooks 所以这里我只使用 use Reducer
。
我唯一的问题是我做得对吗?我对其他方法持开放态度。
我已经在 Vue 中使用 Vuex 完成了身份验证和授权,我用它来创建一个在所有路由上运行的存储文件
我对 Redux 也一样,在我的存储文件中,我用来存储状态和所有内容。
现在,如果我正在使用 react-hooks 和 react-apollo-graphql,那么不需要用 redux 做这些事情。
我正在使用 apollo-link-context
来传递 header(授权),如下所示
const authLink = setContext(() => {
const token = localStorage.getItem('JWT_Token')
return {
headers:{
Authorization: token ? `${token}` : ''
}
}
});
我想在这里我可以检查每个路由或每个请求令牌是否有效(检查 exp 时间)如果它无效然后我将注销并清除我的本地存储,清除存储不是重要的是如何重定向到登录页面。
对于你的问题,解决方案可能是这样的:
- 从上下文中删除授权部分。 (不好的做法)
- 创建一个订阅了
react-router
的组件来检查用户的授权状态。
- 在
main
组件中渲染它。
authverify.component.js
import { withRouter } from "react-router-dom";
const AuthVerifyComponent = ({ history }) => {
history.listen(() => { // <--- Here you subscribe to the route change
if (localStorage.getItem("JWT_Token")) {
const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
console.log(jwt_Token_decoded.exp * 1000);
console.log(Date.now());
if (jwt_Token_decoded.exp * 1000 < Date.now()) {
localStorage.clear();
} else {
initialstate.user = jwt_Token_decoded;
}
}
});
return <div></div>;
};
export default withRouter(AuthVerifyComponent);
app.js
<AuthProvider>
<Router>
<div className="App wrapper">
<Routes />
<AuthVerifyComponent />
</div>
</Router>
</AuthProvider>;
您遇到的问题很简单。您的 AuthReducer 在创建时仅接受一次 initialState。现在,当您重新加载您的应用程序时,所有内容都会再次初始化,并且您的逻辑会处理过期时间。但是在路线更改时它不会重新评估您的初始状态。
然而你可以做的是在使用 setContext
时你可以通过使用 jwtDecode
解码令牌来检查过期验证,如果令牌过期则刷新令牌并保存在 localStorage 因为这是执行根据每个请求
const authLink = setContext(async () => {
let token = localStorage.getItem('JWT_Token')
const { exp } = jwtDecode(token)
// Refresh the token a minute early to avoid latency issues
const expirationTime = (exp * 1000) - 60000
if (Date.now() >= expirationTime) {
token = await refreshToken()
// set LocalStorage here based on response;
}
return {
// you can set your headers directly here based on the new token/old token
headers: {
...
}
}
})
然而,由于您希望在令牌过期时重定向到登录页面而不是刷新令牌,您可以将自定义历史记录对象与路由一起使用
src/history.js
import { createBrowserHistory } from 'history';
const history = createBrowserHistory()
export default history;
App.js
import history from '/path/to/history.js';
import { Router } from 'react-router-dom';
<AuthProvider>
<Router history={history}>
<div className="App wrapper">
<Routes/>
</div>
</Router>
</AuthProvider>
然后在 setContext 中你可以做
import history from '/path/to/history';
const authLink = setContext(async () => {
let token = localStorage.getItem('JWT_Token')
const { exp } = jwtDecode(token)
const expirationTime = (exp * 1000) - 60000
if (Date.now() >= expirationTime) {
localStorage.clear();
history.push('/login');
}
return {
// you can set your headers directly here based on the old token
headers: {
...
}
}
})
受到 Yogesh Aggarwal 回答的启发,我更喜欢这样做:
import React from 'react';
import {useLocation, useHistory} from 'react-router-dom';
const AuthProvider = () => {
const pathName = useLocation().pathname;
const history = useHistory();
if (pathName === your_path_that_need_authentication) {
// if token expired then history.push(login_page));
}
return null;
};
export default AuthProvider;
然后把这个AuthProvider放到项目中。
从历史中提取路径名是一个很好的选择,但不是最好的选择。问题是,如果您键入或 copy/paste 所需的 URL,它将不再起作用,但使用 useLocation 钩子将解决此问题
我正在开发一个应用程序,我使用 React 作为我的 front-end 和 React-apollo-graphql
作为我的 API 调用。
我正在使用 react-hooks
即 React 16.8 +.
我在做什么
我创建了一个 auth.js
文件,当用户登录时我在其中存储我的值,并检查令牌是否有效(我正在检查过期),但该文件仅在加载我正在刷新或重新加载页面,这不是它应该的工作方式
我的auth.js文件
const initialstate = {
user: null,
};
if (localStorage.getItem("JWT_Token")) {
const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
console.log(jwt_Token_decoded.exp * 1000);
console.log(Date.now());
if (jwt_Token_decoded.exp * 1000 < Date.now()) {
localStorage.clear(); // this runs only when I refresh the page or reload on route change it dosent work
} else {
initialstate.user = jwt_Token_decoded;
}
}
const AuthContext = createContext({
user: null,
login: (userData) => {},
logout: () => {},
});
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return {
...state,
user: action.payload,
};
case "LOGOUT":
return {
...state,
user: null,
};
default:
return state;
}
};
const AuthProvider = (props) => {
const [state, dispatch] = useReducer(AuthReducer, initialstate);
const login = (userData) => {
localStorage.setItem("JWT_Token", userData.token);
dispatch({
type: "LOGIN",
payload: userData,
});
};
const logout = () => {
localStorage.clear();
dispatch({ action: "LOGOUT" });
};
return (
<AuthContext.Provider
value={{ user: state.user, login, logout }}
{...props}
/>
);
};
export { AuthContext, AuthProvider };
正如我在检查令牌到期的行中所注释的那样。
我唯一的问题是为什么它在页面重新加载上工作,而不是像我们在使用 Redux 时在存储文件中所做的那样在每个路由上工作。
我的App.js
<AuthProvider>
<Router>
<div className="App wrapper">
<Routes/>
</div>
</Router>
</AuthProvider>
我的index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from '@apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
const client = new ApolloClient({
uri: 'my url',
cache: new InMemoryCache(),
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
要点
因为我正在使用 react-apollo-graphql
所以他们提供 ant 身份验证流程吗?就像 redux 一样,我们必须创建一个存储文件来存储我们的数据
我使用的是 React 16.8 + 所以我使用的是 react-hooks 所以这里我只使用 use Reducer
。
我唯一的问题是我做得对吗?我对其他方法持开放态度。
我已经在 Vue 中使用 Vuex 完成了身份验证和授权,我用它来创建一个在所有路由上运行的存储文件
我对 Redux 也一样,在我的存储文件中,我用来存储状态和所有内容。
现在,如果我正在使用 react-hooks 和 react-apollo-graphql,那么不需要用 redux 做这些事情。
我正在使用 apollo-link-context
来传递 header(授权),如下所示
const authLink = setContext(() => {
const token = localStorage.getItem('JWT_Token')
return {
headers:{
Authorization: token ? `${token}` : ''
}
}
});
我想在这里我可以检查每个路由或每个请求令牌是否有效(检查 exp 时间)如果它无效然后我将注销并清除我的本地存储,清除存储不是重要的是如何重定向到登录页面。
对于你的问题,解决方案可能是这样的:
- 从上下文中删除授权部分。 (不好的做法)
- 创建一个订阅了
react-router
的组件来检查用户的授权状态。 - 在
main
组件中渲染它。
authverify.component.js
import { withRouter } from "react-router-dom";
const AuthVerifyComponent = ({ history }) => {
history.listen(() => { // <--- Here you subscribe to the route change
if (localStorage.getItem("JWT_Token")) {
const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
console.log(jwt_Token_decoded.exp * 1000);
console.log(Date.now());
if (jwt_Token_decoded.exp * 1000 < Date.now()) {
localStorage.clear();
} else {
initialstate.user = jwt_Token_decoded;
}
}
});
return <div></div>;
};
export default withRouter(AuthVerifyComponent);
app.js
<AuthProvider>
<Router>
<div className="App wrapper">
<Routes />
<AuthVerifyComponent />
</div>
</Router>
</AuthProvider>;
您遇到的问题很简单。您的 AuthReducer 在创建时仅接受一次 initialState。现在,当您重新加载您的应用程序时,所有内容都会再次初始化,并且您的逻辑会处理过期时间。但是在路线更改时它不会重新评估您的初始状态。
然而你可以做的是在使用 setContext
时你可以通过使用 jwtDecode
解码令牌来检查过期验证,如果令牌过期则刷新令牌并保存在 localStorage 因为这是执行根据每个请求
const authLink = setContext(async () => {
let token = localStorage.getItem('JWT_Token')
const { exp } = jwtDecode(token)
// Refresh the token a minute early to avoid latency issues
const expirationTime = (exp * 1000) - 60000
if (Date.now() >= expirationTime) {
token = await refreshToken()
// set LocalStorage here based on response;
}
return {
// you can set your headers directly here based on the new token/old token
headers: {
...
}
}
})
然而,由于您希望在令牌过期时重定向到登录页面而不是刷新令牌,您可以将自定义历史记录对象与路由一起使用
src/history.js
import { createBrowserHistory } from 'history';
const history = createBrowserHistory()
export default history;
App.js
import history from '/path/to/history.js';
import { Router } from 'react-router-dom';
<AuthProvider>
<Router history={history}>
<div className="App wrapper">
<Routes/>
</div>
</Router>
</AuthProvider>
然后在 setContext 中你可以做
import history from '/path/to/history';
const authLink = setContext(async () => {
let token = localStorage.getItem('JWT_Token')
const { exp } = jwtDecode(token)
const expirationTime = (exp * 1000) - 60000
if (Date.now() >= expirationTime) {
localStorage.clear();
history.push('/login');
}
return {
// you can set your headers directly here based on the old token
headers: {
...
}
}
})
受到 Yogesh Aggarwal 回答的启发,我更喜欢这样做:
import React from 'react';
import {useLocation, useHistory} from 'react-router-dom';
const AuthProvider = () => {
const pathName = useLocation().pathname;
const history = useHistory();
if (pathName === your_path_that_need_authentication) {
// if token expired then history.push(login_page));
}
return null;
};
export default AuthProvider;
然后把这个AuthProvider放到项目中。
从历史中提取路径名是一个很好的选择,但不是最好的选择。问题是,如果您键入或 copy/paste 所需的 URL,它将不再起作用,但使用 useLocation 钩子将解决此问题