服务器:Node.js,express,Type-Graphql with Apollo Server
在index.ts
中:
import 'reflect-metadata';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { createConnection } from 'typeorm';
import { verify } from 'jsonwebtoken';
import coockieParser from 'cookie-parser';
import cors from 'cors';
import User from './entity/User';
import UserResolver from './resolvers';
import { createAccessToken, createRefreshToken, sendRefreshToken } from './auth';
require('dotenv').config();
const corsOptions = {
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'X-Access-Token', 'Authorization'],
credentials: true, // this allows to send back (to client) cookies
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
origin: 'http://localhost:3000',
preflightContinue: false,
};
(async () => {
const PORT = process.env.PORT || 4000;
const app = express();
app.use(coockieParser());
app.use(cors(corsOptions));
// -- non graphql endpoints
app.get('/', (_, res) => {
res.send('Starter endpoint');
});
app.post('/refresh_token', async (req, res) => {
const token = req.cookies.jid;
if (!token) {
return res.send({ ok: false, accessToken: '' });
}
let payload: any = null;
try {
payload = verify(token, process.env.REFRESH_TOKEN_SECRET!);
} catch (e) {
console.log(e);
return res.send({ ok: false, accessToken: '' });
}
// token is valid, and the access token can be send back
const user = await User.findOne({ id: payload.userId });
if (!user) {
return res.send({ ok: false, accessToken: '' });
}
if (user.tokenVersion !== payload.tokenVersion) {
return res.send({ ok: false, accessToken: '' });
}
sendRefreshToken(res, createRefreshToken(user));
return res.send({ ok: true, accessToken: createAccessToken(user) });
});
//--
// -- db
await createConnection();
// --
// -- apollo server settings
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [UserResolver],
}),
context: ({ req, res }) => ({ req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: false,
});
// --
app.listen(PORT, () => {
console.log(`Server running on port: ${PORT}`);
});
})();
UserResolver
中的登录突变:
//..
@Mutation(() => LoginResponse)
async login(
@Arg('email') email: string,
@Arg('password') password: string,
@Ctx() { res }: AuthContext,
): Promise<LoginResponse> {
const user = await User.findOne({ where: { email } });
if (!user) {
throw new Error('Incorrect email');
}
const valid = await compare(password, user.password);
if (!valid) {
throw new Error('Incorrect password');
}
sendRefreshToken(res, createRefreshToken(user));
return {
accessToken: createAccessToken(user),
user,
};
}
//..
在处理身份验证时,cookie在响应头中设置如下:
//..
export const createAccessToken = (user: User) => sign({ userId: user.id }, process.env.ACCESS_TOKEN_SECRET!, { expiresIn: '10m' });
export const createRefreshToken = (user: User) => sign({ userId: user.id, tokenVersion: user.tokenVersion }, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: '7d' });
export const sendRefreshToken = (res: Response, refreshToken: string) => {
res.cookie('jid', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/refresh_token',
});
};
//..
客户端:Next.js,Graphql with URQL
在_app.tsx
中:
/* eslint-disable react/jsx-props-no-spreading */
import * as React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/app';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import { createClient, Provider } from 'urql';
import theme from '../styles/theme';
import createEmotionCache from '../lib/createEmotionCache';
import '../styles/globals.css';
// Client-side cache shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
interface IAppProps extends AppProps {
// eslint-disable-next-line react/require-default-props
emotionCache?: EmotionCache;
}
const client = createClient({
url: 'http://localhost:4000/graphql',
fetchOptions: {
credentials: 'include',
},
});
const App = (props: IAppProps) => {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
return (
<Provider value={client}>
<CacheProvider value={emotionCache}>
<Head>
<title>Client App</title>
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
</Provider>
);
};
export default App;
登录页面不依赖于SSR或SSG(因此是CSR):
import React from 'react';
import LoginForm from '../components/LoginForm/LoginForm';
import Layout from '../layouts/Layout';
interface ILoginProps {}
const Login: React.FC<ILoginProps> = () => (
<Layout
showNavbar={false}
showTransition={false}
maxWidth='xs'
>
<LoginForm />
</Layout>
);
export default Login;
在LoginForm组件中使用该变化来请求访问令牌并在浏览器cookie中设置刷新令牌:
import React from 'react';
import { useRouter } from 'next/router';
import { useLoginMutation } from '../../generated/graphql';
//...
const LoginForm = () => {
//..
const [, login] = useLoginMutation();
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (disabledSubmit) {
setShowFormHelper(true);
} else {
const res = await login({
email, // from the state of the component
password,
});
if (res && res.data?.login) {
console.log(res.data.login.accessToken);
router.push('/home');
setShowFormHelper(false);
} else {
setHelper('Something went wrong');
}
}
};
//..
};
export default LoginForm;
问题
所以,问题是登录响应在头文件中设置了-cookie,但浏览器中仍然没有设置cookie:
提问
之前,我已经使用相同的服务器代码实现了相同的身份验证方案,但客户端使用create-react-app。一切都很好。那么,* 为什么它现在不能用next.js呢?我错过了什么?*
解决方案
我可以使用类似cookies-next的东西来将饼干放入存储器中。然后需要在响应数据中传递刷新令牌:
import React from 'react';
import { useRouter } from 'next/router';
import { useLoginMutation } from '../../generated/graphql';
import { setCookies } from 'cookies-next';
//...
const LoginForm = () => {
//..
const [, login] = useLoginMutation();
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (disabledSubmit) {
setShowFormHelper(true);
} else {
const res = await login({
email, // from the state of the component
password,
});
if (res && res.data?.login) {
console.log(res.data.login.accessToken);
setCookies('jid', res.data.login.refreshToken);
router.push('/home');
setShowFormHelper(false);
} else {
setHelper('Something went wrong');
}
}
};
//..
};
export default LoginForm;
setCookie
接受选项。但是,在这种情况下,httpOnly
不能设置为true
。
更新
事实证明,上面的一切都可以在Firefox中运行,但在Chrome中不行。
1条答案
按热度按时间yizd12fk1#
默认情况下,浏览器对跨域请求和cookie有不同的策略,特别是在使用
Secure
标志和SameSite
属性设置cookie时,因此浏览器体验介于Firefox
和Chrome
之间。要解决登录响应的
Set-Cookie
标头未在浏览器中设置cookie的问题,请尝试修改服务器代码中的sendRefreshToken
函数。在
sendRefreshToken
函数中,设置cookie时将res.cookie
块中的sameSite
属性从strict
更改为lax
。此更改可以解决问题: