【发布时间】:2021-01-30 10:11:15
【问题描述】:
我目前正在使用 Nextjs 和使用 Expressjs 的 api 实现身份验证流程。
我希望将JWT token 存储为身份验证令牌in memory,我可以使用存储在HTTPOnly cookie 中的刷新令牌定期刷新它。
对于我的实现,我参考了 nice OSS 项目here。
我的问题是,当我在登录期间将身份验证令牌存储在inMemoryToken 中时,该值仅存储在可用的客户端但仍然可用的服务器端,反之亦然。
另一个例子是当我断开连接时:
-
inMemoryToken等于服务器端的东西 - 用户单击注销按钮,
logout()被称为前端,inMemoryToken = null - 在页面更改时,服务器上调用了
getServerSideProps(),但服务器上的inMemoryToken仍等于之前的值,因此我的用户仍显示为已连接。
这里是Nextjs 代码
//auth.js
import { Component } from 'react';
import Router from 'next/router';
import { serialize } from 'cookie';
import { logout as fetchLogout, refreshToken } from '../services/api';
let inMemoryToken;
export const login = ({ accessToken, accessTokenExpiry }, redirect) => {
inMemoryToken = {
token: accessToken,
expiry: accessTokenExpiry,
};
if (redirect) {
Router.push('/');
}
};
export const logout = async () => {
inMemoryToken = null;
await fetchLogout();
window.localStorage.setItem('logout', Date.now());
Router.push('/');
};
const subMinutes = (dt, minutes) => {
return new Date(dt.getTime() - minutes * 60000);
};
export const withAuth = (WrappedComponent) => {
return class extends Component {
static displayName = `withAuth(${Component.name})`;
state = {
accessToken: this.props.accessToken,
};
async componentDidMount() {
this.interval = setInterval(async () => {
inMemoryToken = null;
const token = await auth();
inMemoryToken = token;
this.setState({ accessToken: token });
}, 60000);
window.addEventListener('storage', this.syncLogout);
}
componentWillUnmount() {
clearInterval(this.interval);
window.removeEventListener('storage', this.syncLogout);
window.localStorage.removeItem('logout');
}
syncLogout(event) {
if (event.key === 'logout') {
Router.push('/');
}
}
render() {
return (
<WrappedComponent
{...this.props}
accessToken={this.state.accessToken}
/>
);
}
};
};
export const auth = async (ctx) => {
console.log('auth ', inMemoryToken);
if (!inMemoryToken) {
inMemoryToken = null;
const headers =
ctx && ctx.req
? {
Cookie: ctx.req.headers.cookie ?? null,
}
: {};
await refreshToken(headers)
.then((res) => {
if (res.status === 200) {
const {
access_token,
access_token_expiry,
refresh_token,
refresh_token_expiry,
} = res.data;
if (ctx && ctx.req) {
ctx.res.setHeader(
'Set-Cookie',
serialize('refresh_token', refresh_token, {
path: '/',
expires: new Date(refresh_token_expiry),
httpOnly: true,
secure: false,
}),
);
}
login({
accessToken: access_token,
accessTokenExpiry: access_token_expiry,
});
} else {
let error = new Error(res.statusText);
error.response = res;
throw error;
}
})
.catch((e) => {
console.log(e);
if (ctx && ctx.req) {
ctx.res.writeHead(302, { Location: '/auth' });
ctx.res.end();
} else {
Router.push('/auth');
}
});
}
const accessToken = inMemoryToken;
if (!accessToken) {
if (!ctx) {
Router.push('/auth');
}
}
return accessToken;
};
//page index.js
import Head from 'next/head';
import { Layout } from '../components/Layout';
import { Navigation } from '../components/Navigation';
import { withAuth, auth } from '../libs/auth';
const Home = ({ accessToken }) => (
<Layout>
<Head>
<title>Home</title>
</Head>
<Navigation />
<div>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
</Layout>
);
export const getServerSideProps = async (ctx) => {
const accessToken = await auth(ctx);
return {
props: { accessToken: accessToken ?? null },
};
};
export default withAuth(Home);
部分快递js代码:
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
....
const refreshToken = uuidv4();
const refreshTokenExpiry = new Date(new Date().getTime() + 10 * 60 * 1000);
res.cookie('refresh_token', refreshToken, {
maxAge: 10 * 60 * 1000,
httpOnly: true,
secure: false,
});
res.json({
access_token: accessToken,
access_token_expiry: accessTokenExpiry,
refresh_token: refreshToken,
user,
});
});
app.post('/api/refresh-token', (req, res) => {
const refreshToken = req.cookies['refresh_token'];
.....
const newRefreshToken = uuidv4();
const newRefreshTokenExpiry = new Date(
new Date().getTime() + 10 * 60 * 1000,
);
res.cookie('refresh_token', newRefreshToken, {
maxAge: 10 * 60 * 1000,
httpOnly: true,
secure: false,
});
res.json({
access_token: accessToken,
access_token_expiry: accessTokenExpiry,
refresh_token: newRefreshToken,
refresh_token_expiry: newRefreshTokenExpiry,
});
});
app.post('/api/logout', (_, res) => {
res.clearCookie('refresh_token');
res.sendStatus(200);
});
我的理解是,即使let inMemoryToken 被声明一次,它的两个单独的实例将在运行时可用,一个客户端和一个服务器端,并且修改不会影响另一个。
我说的对吗?
在这种情况下,auth方法既可以在服务端调用,又可以在客户端调用,如何解决?
【问题讨论】:
标签: javascript reactjs authentication cookies next.js