本文介绍了Angular 10 SSR和expressjs会话的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在Angular中有一个组件,我在其中使用HttpClient向服务器发出GET请求以获取当前登录的用户.由于这是一个SSR应用程序,因此代码可同时在客户端和服务器上运行.问题在于,当它在服务器上运行时,会话数据不可用,这意味着无法验证对后端的请求,因此失败.在客户端上,会话数据可用,因此请求成功.

I have a component in Angular where I'm using HttpClient to do a GET request to the server to get the currently signed in user. Since this is an SSR app the code runs both on the client and the server. The problem is that when it runs on the server, the session data is not available, which means that the request to the backend can't be authenticated, so it fails. On the client the session data is available so the request succeeds.

我将 express-session 与以下会话选项一起使用:

I use express-session with the following session options:

const sessionOptions: session.SessionOptions = {
  secret: process.env.SESSION_SECRET || 'placeholder',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false },
};
server.use(session(sessionOptions));

我使用Twitter OAuth进行身份验证.

I use Twitter OAuth for authentication.

const router = Router();

router.get('/sessions/connect', (req, res) => {
  const twitterAuth = new TwitterAuth(req);
  twitterAuth.consumer.getOAuthRequestToken((error, oauthToken, oauthTokenSecret, _) => {
    if (error) {
      console.error('Error getting OAuth request token:', error);
      res.sendStatus(500);
    } else {
      req.session.oauthRequestToken = oauthToken;
      req.session.oauthRequestTokenSecret = oauthTokenSecret;
      res.redirect(`https://twitter.com/oauth/authorize?oauth_token=${oauthToken}`);
    }
  });
});

router.get('/sessions/disconnect', (req, res) => {
  req.session.oauthRequestToken = null;
  req.session.oauthRequestTokenSecret = null;
  res.redirect('/');
});

router.get('/sessions/callback', (req, res) => {
  const twitterAuth = new TwitterAuth(req);
  const oauthVerifier = req.query.oauth_verifier as string;
  twitterAuth.consumer.getOAuthAccessToken(
    req.session.oauthRequestToken,
    req.session.oauthRequestTokenSecret,
    oauthVerifier,
    async (error, oauthAccessToken, oauthAccessTokenSecret, results) => {
      if (error) {
        console.error('Error getting OAuth access token:', error, `[${oauthAccessToken}] [${oauthAccessTokenSecret}] [${results}]`);
        res.sendStatus(500);
      } else {
        req.session.oauthAccessToken = oauthAccessToken;
        req.session.oauthAccessTokenSecret = oauthAccessTokenSecret;

        const twitter = twitterAuth.api(req.session);
        try {
          const response = await twitter.get('account/verify_credentials', {});
          const screenName = response.screen_name;

          console.log(`Signed in with @${screenName}`);
          console.log(response);

          res.redirect('/');
        } catch (err) {
          console.error(err);
          res.sendStatus(500);
        }
      }
    }
  );
});

router.get('/user', async (req, res) => {
  const twitterAuth = new TwitterAuth(req);
  const twitter = twitterAuth.api(req.session);
  try {
    const response = await twitter.get('account/verify_credentials', {});
    res.json({
      name: response.name,
      screenName: response.screen_name,
    });
  } catch (err) {
    console.error(err);
    res.sendStatus(401);
  }
});

在客户端上,GET请求看起来像这样:

On the client the GET request looks something like this:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { User } from '../types/user';

const USER_KEY = makeStateKey('user');

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.sass']
})
export class HomeComponent implements OnInit {
  user?: User;

  constructor(private httpClient: HttpClient, private state: TransferState) {}

  ngOnInit(): void {
    this.user = this.state.get(USER_KEY, null);

    if (!this.user) {
      this.httpClient.get('/api/twitter/user').pipe(catchError(this.handleHttpError)).subscribe((user: User) => {
        this.user = user;
        this.state.set(USER_KEY, user);
      });
    }
  }

  // [irrelevant code omitted]
}

这个想法是,首先在服务器上执行GET请求,然后使用TransferState保存用户,以便一旦在客户端上再次运行相同的代码,便可以将其提供给客户端.但是,问题在于服务器上的请求失败,并显示以下错误:

The idea is that the GET request is first executed on the server, then the user is saved using TransferState so that it'll be made available to the client once the same code runs again on the client. The problem, though, is that the request fails on the server with the following error:

ERROR HttpErrorResponse {
  headers: HttpHeaders {
    normalizedNames: Map(0) {},
    lazyUpdate: null,
    lazyInit: [Function (anonymous)]
  },
  status: 401,
  statusText: 'Unauthorized',
  url: 'https://<domain>/api/twitter/user',
  ok: false,
  name: 'HttpErrorResponse',
  message: 'Http failure response for https://<domain>/api/twitter/user: 401 Unauthorized',
  error: 'Unauthorized'

当我为客户端GET调用和服务器GET调用console.log expressjs request.session对象时,我注意到服务器GET调用具有不同的会话ID,因此缺少用于身份验证的令牌和令牌密钥请求.如何确保客户端和服务器都共享相同的会话ID和相同的令牌?

When I console.log the expressjs request.session object for the client GET call and the server GET call, I notice that the server GET call has a different session ID, and that it thus lacks the tokens and token secrets to authenticate the request. How can I make sure that both the client and the server share the same session ID and the same tokens?

推荐答案

我找不到使它与会话一起使用的方法,但是我确实找到了一种仅使用cookie使其工作的方法.

I couldn't find a way to get this to work with sessions, but I did find a way to make it work by using just cookies.

import { APP_BASE_HREF } from '@angular/common';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import * as cookieParser from 'cookie-parser';
import * as cookieEncrypter from 'cookie-encrypter';

// …

server.use(cookieParser(cookieSecret));
server.use(cookieEncrypter(cookieSecret));

// All regular routes use the Universal engine
server.get('*', (req, res) => {
  res.render(indexHtml, {
    req,
    res,
    providers: [
      { provide: APP_BASE_HREF, useValue: req.baseUrl },
      { provide: REQUEST, useValue: req },
      { provide: RESPONSE, useValue: res }
    ]
  });
});

然后,对于auth端点,我这样做:

Then, for the auth endpoints I did this:

const cookieOptions: CookieOptions = {
  httpOnly: true,
  signed: true,
};

const router = Router();

router.get('/auth/connect', (req, res) => {
  const twitterAuth = new TwitterAuth(req);
  twitterAuth.consumer.getOAuthRequestToken((error, oauthToken, oauthTokenSecret, _) => {
    if (error) {
      console.error('Error getting OAuth request token:', error);
      res.sendStatus(500);
    } else {
      res.cookie('oauthRequestToken', oauthToken, cookieOptions);
      res.cookie('oauthRequestTokenSecret', oauthTokenSecret, cookieOptions);
      res.redirect(`https://twitter.com/oauth/authorize?oauth_token=${oauthToken}`);
    }
  });
});

router.get('/auth/disconnect', (req, res) => {
  res.clearCookie('oauthRequestToken', { signed: true });
  res.clearCookie('oauthRequestTokenSecret', { signed: true });
  res.clearCookie('oauthAccessToken', { signed: true });
  res.clearCookie('oauthAccessTokenSecret', { signed: true });
  res.redirect('/');
});

router.get('/auth/callback', (req, res) => {
  const twitterAuth = new TwitterAuth(req);
  const oauthVerifier = req.query.oauth_verifier as string;
  twitterAuth.consumer.getOAuthAccessToken(
    req.signedCookies.oauthRequestToken,
    req.signedCookies.oauthRequestTokenSecret,
    oauthVerifier,
    async (error, oauthAccessToken, oauthAccessTokenSecret, results) => {
      if (error) {
        console.error('Error getting OAuth access token: ', error);
        res.sendStatus(error.statusCode);
      } else {
        res.cookie('oauthAccessToken', oauthAccessToken, cookieOptions);
        res.cookie('oauthAccessTokenSecret', oauthAccessTokenSecret, cookieOptions);

        const twitter = twitterAuth.api({ oauthAccessToken, oauthAccessTokenSecret });
        try {
          const response = await twitter.get('account/verify_credentials', {});
          const screenName = response.screen_name;

          console.log(`Signed in with @${screenName}`);
          console.log(response);

          res.redirect('/');
        } catch (err) {
          console.error(err);
          res.sendStatus(500);
        }
      }
    }
  );
});

router.get('/user', async (req, res) => {
  const twitterAuth = new TwitterAuth(req);
  const twitter = twitterAuth.api(req.signedCookies);
  try {
    const response = await twitter.get('account/verify_credentials', {});
    res.json({
      name: response.name,
      screenName: response.screen_name,
    });
  } catch (err) {
    console.error(err);
    res.sendStatus(401);
  }
});

在客户端方面并没有真正改变:

On the client side much hasn't really changed:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { User } from '../types/user';

const USER_KEY = makeStateKey('user');

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.sass']
})
export class HomeComponent implements OnInit {
  user?: User;

  constructor(private httpClient: HttpClient, private state: TransferState) {}

  ngOnInit(): void {
    this.user = this.state.get(USER_KEY, null);

    if (!this.user) {
      // The request path must be absolute as opposed to relative
      this.httpClient.get('https://<domain>/api/twitter/user').pipe(catchError(this.handleHttpError)).subscribe((user: User) => {
        this.user = user;
        this.state.set(USER_KEY, user);
      });
    }
  }

  // [irrelevant code omitted]
}

为了将cookie暴露给服务器,我必须创建以下拦截器:

In order to expose the cookies to the server I had to create the following interceptor:

import { Injectable, Optional, Inject } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent } from '@angular/common/http';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Observable } from 'rxjs';

@Injectable()
export class CookieInterceptor implements HttpInterceptor {
  constructor(@Optional() @Inject(REQUEST) private httpRequest) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // If optional request is provided, we are server side
    if (this.httpRequest) {
      req = req.clone({
        setHeaders: { Cookie: this.httpRequest.headers.cookie }
      });
    }
    return next.handle(req);
  }
}

然后,我必须将其添加到 app.server.module.ts :

Then, I had to add it in app.server.module.ts:

import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { CookieBackendModule } from 'ngx-cookie-backend';
import { HttpClientModule, XhrFactory, HTTP_INTERCEPTORS } from '@angular/common/http';
import * as xhr2 from 'xhr2';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { CookieInterceptor } from '../../cookie-interceptor';

class ServerXhr implements XhrFactory {
  build(): XMLHttpRequest {
    xhr2.XMLHttpRequest.prototype._restrictedHeaders = {};
    return new xhr2.XMLHttpRequest();
  }
}

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
    HttpClientModule,
    CookieBackendModule.forRoot(),
  ],
  bootstrap: [AppComponent],
  providers: [
    { provide: XhrFactory, useClass: ServerXhr },
    { provide: HTTP_INTERCEPTORS, useClass: CookieInterceptor, multi: true }
  ],
})
export class AppServerModule {}

注意 ServerXhr 类,该类也需要添加为提供程序.

Notice the ServerXhr class that also needs to be added as a provider.

此答案启发的解决方案.

这篇关于Angular 10 SSR和expressjs会话的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-04 04:52
查看更多