javascript - Nextjs - 存储在内存中的身份验证 token + 仅 HTTP cookie 中的刷新 token

标签 javascript reactjs authentication cookies next.js

我目前正在使用 Nextjs 实现身份验证流程和一个使用 Expressjs 的 api。
我正在寻找存储 JWT token作为身份验证 token in memory我可以使用存储在 HTTPOnly cookie 中的刷新 token 定期刷新.
对于我的实现,我引用了 nice OSS 项目 here .
我的问题是,当我将身份验证 token 存储在 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);
    
    部分 express 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 方法既可以在服务器上调用,也可以在客户端上调用,如何解决这个问题?

    最佳答案

    我创建了一个示例,展示了如何使用 session 在内存中为单个用户跨请求存储信息。如果您只是对代码感兴趣,可以查看底部的代码框。
    有两件事要记住:

  • 你不能在你的客户端上直接访问你服务器上声明的变量
  • 在服务器上声明的全局范围内的变量在请求之间共享

  • 但是,您可以使用存储在客户端 cookie 中的通用 ID,并将数据存储在内存中,并通过请求上的 session 访问它。
    当您收到请求时,您可以检查 cookie 是否存在于请求的 header 中,如果存在则尝试从内存中加载 session ,如果不存在或无法加载,则创建一个新 session 。
    | Incoming Request
    |   |--> Check the cookie header for your session key
    |       |--> If cookie exists load cookie
    |       |--> Else create session + use 'set-cookie' header to tell the client it's session key
    |          |--> Do stuff with the data stored in the session  
    
    为了能够做到这一点,我们需要有一些方法来存储 session 和与之关联的数据。您说您只想将数据存储在内存中。
    const memoryStore = new Map();
    
    好的,现在我们有了内存存储,但是我们如何让它在请求中保持不变呢?让我们将它存储为一个全局对象。
    const MEMORY_STORE = Symbol.for('__MEMORY_STORE');
    const getMemoryStore = () => {
      if (!global[MEMORY_STORE]) {
        global[MEMORY_STORE] = new Map();
      }
      return global[MEMORY_STORE];
    };
    
    完美,现在我们可以调用getMemoryStore访问持久化的数据。现在我们要创建一个处理程序,尝试从请求中加载 session ,否则创建一个新 session 。
    const SESSION_KEY = '__my_session_id';
    
    const loadSession = (req, res) => {
      const memory = getMemoryStore();
    
      const cookies = parseCookies(req.headers.cookie);
      const cookieSession = cookies[SESSION_KEY];
    
      // check to make sure that cookieSession is defined and that it exists in the memory store
      if (cookieSession && memory.has(cookieSession)) {
        const session = memory.get(cookieSession);
        req.session = session;
        
        // do something with the session
    
      } else {
        // okay the session doesn't exists so we need to create one, create the unique session id
        const sessionId = uuid();
        const session = { id: sessionId };
        memory.set(sessionId, session);
       
        // set the set-cookie header on the response with the session ID
        res.setHeader('set-cookie', `${SESSION_KEY}=${sessionId}`);
    
        req.session = session;
      }  
    };
    
    现在我们可以在服务器端的任何地方调用它,它将加载或创建 session 。例如,您可以从 getServerSideProps 调用它
    export const getServerSideProps = ({ req, res }) => {
      loadSession(req, res);
    
      // our session exists on req.session !! 
      
      return { props: { ... } };
    };
    
    我制作了一个带有工作示例的代码框:https://codesandbox.io/s/distracted-water-biicc?file=/utils/app.js

    关于javascript - Nextjs - 存储在内存中的身份验证 token + 仅 HTTP cookie 中的刷新 token ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64379817/

    相关文章:

    javascript - 如何处理不断变化的嵌套 api 调用

    spring - 我如何获得所有登录用户的列表(通过 Spring Security)我的 Web 应用程序

    python - Jupyterhub : where is get_config defined and how do I create a custom Authenticator?

    javascript - 在我的 Jquery 插件中执行循环的更好方法?

    javascript - meteor 集合.update 没有更新文档

    javascript - 选择 html 元素文本作为 JavaScript 的输入

    javascript - Coffeescript 未捕获引用

    reactjs - 如何在 Material ui文本字段的输入中修复占位符文本

    javascript - 如何在Crossfilter中正确创建维度和组?

    Java - HttpClient 库的 Http 身份验证 401 错误