Экспресс-сессия не постоянна на обратном прокси NGINX - PullRequest
0 голосов
/ 06 июля 2019

Я хочу, чтобы пользователи могли войти в Discord и сохранить их в сеансе с express-session. Когда я запускаю фронтенд и бэкэнд локально, он отлично работает, но когда я разверну его на моем сервере Digitalocean с NGINX, сеанс не будет продолжаться. Клиент никогда не получает cookie, необходимый для сохранения сеанса.

Это настройка для входа в сеанс с использованием express-session, passport и MySQL для хранения сеанса.

import express, { Request, NextFunction, Response } from 'express';
import cors from 'cors';
import session, { SessionOptions } from 'express-session';
import DiscordStrategy from 'passport-discord';
import passport from 'passport';
import mysqlSession from 'express-mysql-session';
import secretConfig from 'config/secret';
import discordConfig from 'config/discord';
import apiConfig from 'config/apiconfig';
import { RESPONSE_CODE } from './helpers';

const isProd = process.env.NODE_ENV === 'production';

const app = express();

if (isProd) {
  app.set('trust proxy', 1); // Trust first proxy
  app.disable('x-powered-by'); // Hide information about the server
}

// Enable CORS
app.use(cors({
  credentials: true,
  origin: (origin, callback) => {
    const sameServer = !origin;

    if (sameServer || apiConfig.CORSWhitelist.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
}));

passport.serializeUser((user, done) => {
  done(null, user);
});
passport.deserializeUser((user, done) => {
  done(null, user);
});

passport.use(new DiscordStrategy(
  {
    clientID: secretConfig.discord.publicKey,
    clientSecret: secretConfig.discord.privateKey,
    callbackURL: discordConfig.callbackUrl,
    scope: discordConfig.scopes,
  },
  (accessToken, refreshToken, user, done) => {
    process.nextTick(() => {
      return done(null, user);
    });
  }
));

const MysqlStore = mysqlSession(session);
const mysqlCfg = {
  host: 'localhost',
  port: 3306,
  user: 'user',
  password: 'password',
  database: 'database',
};

const sessionCfg: SessionOptions = {
  secret: secretConfig.sessionSecret,
  name: 'plan-b-auth',
  resave: false,
  saveUninitialized: false,
  proxy: isProd,
  cookie: {
    secure: isProd,
  },
  store: new MysqlStore(mysqlCfg),
};

app.use(session(sessionCfg));
app.use(passport.initialize());
app.use(passport.session());

const checkAuth = (req: Request, res: Response, next: NextFunction) => {
  if (req.isAuthenticated()) return next();

  res.status(RESPONSE_CODE.UNAUTHORIZED).json('Unauthorized');
};

app.get(
  '/discord/auth',
  passport.authenticate('discord', { scope: discordConfig.scopes }),
);

app.get(
  '/discord/auth/callback',
  passport.authenticate('discord', { failureRedirect: apiConfig.websiteDomain }),
  (req, res) => {
    res.redirect(apiConfig.websiteDomain);
  }
);

app.get(
  '/discord/auth/logout',
  (req, res) => {
    req.logout();
    res.redirect('/');
  }
);

app.get(
  '/discord/auth/me',
  checkAuth,
  (req, res) => {
    res.json(req.user);
  }
);

app.listen(apiConfig.port, (err) => {
  if (err) return console.info(err);
  console.info('Listening at http://localhost:8080/');
});

На веб-интерфейсе (используя Next.js) я получаю пользовательские данные с помощью этой выборки

fetch('https://myapi.com/discord/auth/me', {
    headers: {
      cookie: req.headers.cookie,
    },
    credentials: 'include',
  })

Это мой серверный блок для API (и клиента, они похожи)

server {
        server_name myapi.com;

        location / {
                proxy_pass http://127.0.0.1:3002;
                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;
                proxy_redirect off;

                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

Опять же: когда я запускаю код локально и выполняю вход, он работает. Когда я развертываю его на моем сервере Digitalocean, он больше не работает. На развернутом веб-сайте на клиенте не хранятся файлы cookie, но при локальном запуске они присутствуют. Однако при развертывании сеансы хранятся в моей базе данных MySQL.

1 Ответ

0 голосов
/ 08 июля 2019

Next.js - приложение, отображаемое на стороне сервера. У вас не будет доступа к заголовкам запросов из браузера.

Итак, ваш вопрос о том, как установить куки для Next.js.

Вы должны использовать next-cookies для доступа к нему.

См. Каталог примеров для cookie-auth

Вы должны реализовать пример cookie-auth для доступа к cookie как на клиенте, так и на сервере. Следующие вспомогательные функции хранят и извлекают файлы cookie для аутентифицированных пользователей:

import { Component } from 'react'
import Router from 'next/router'
import nextCookie from 'next-cookies'
import cookie from 'js-cookie'

export const login = async ({ token }) => {
  cookie.set('token', token, { expires: 1 })
  Router.push('/profile')
}

export const logout = () => {
  cookie.remove('token')
  // to support logging out from all windows
  window.localStorage.setItem('logout', Date.now())
  Router.push('/login')
}

// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
  Component.displayName || Component.name || 'Component'

export const withAuthSync = WrappedComponent =>
  class extends Component {
    static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`

    static async getInitialProps (ctx) {
      const token = auth(ctx)

      const componentProps =
        WrappedComponent.getInitialProps &&
        (await WrappedComponent.getInitialProps(ctx))

      return { ...componentProps, token }
    }

    constructor (props) {
      super(props)

      this.syncLogout = this.syncLogout.bind(this)
    }

    componentDidMount () {
      window.addEventListener('storage', this.syncLogout)
    }

    componentWillUnmount () {
      window.removeEventListener('storage', this.syncLogout)
      window.localStorage.removeItem('logout')
    }

    syncLogout (event) {
      if (event.key === 'logout') {
        console.log('logged out from storage!')
        Router.push('/login')
      }
    }

    render () {
      return <WrappedComponent {...this.props} />
    }
  }

export const auth = ctx => {
  const { token } = nextCookie(ctx)

  /*
   * This happens on server only, ctx.req is available means it's being
   * rendered on server. If we are on server and token is not available,
   * means user is not logged in.
   */
  if (ctx.req && !token) {
    ctx.res.writeHead(302, { Location: '/login' })
    ctx.res.end()
    return
  }

  // We already checked for server. This should only happen on client.
  if (!token) {
    Router.push('/login')
  }

  return token
}

Вы должны обернуть свою страницу withAuthSync и использовать куки для доступа к аутентифицированным данным.

class YourPage extends Component {
...
}
YourPage.getInitialProps = async ctx => {
  const { token } = nextCookie(ctx)
  const url = `${process.env.API_URL}/discord/auth/me.js`

  const redirectOnError = () =>
    typeof window !== 'undefined'
      ? Router.push('/login')
      : ctx.res.writeHead(302, { Location: '/login' }).end()

  try {
    const response = await fetch(url, {
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        cookie: token
      }
    })

    if (response.ok) {
      return await response.json()
    }

    return redirectOnError()
  } catch (error) {
    // Implementation or Network error
    return redirectOnError()
  }
}
export default withAuthSync(YourPage)

Также вы должны вызывать функции login и logout после успешной аутентификации через Discord API. Смотрите рабочие примеры в папке github examples, размещенной выше.

...