【问题标题】:Angular 10 SSR and expressjs sessionsAngular 10 SSR 和 expressjs 会话
【发布时间】:2020-12-15 01:33:57
【问题描述】:

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

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

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

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

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 请求如下所示:

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 保存用户,这样一旦相同的代码在客户端上再次运行,它就可供客户端使用。但是,问题是请求在服务器上失败并出现以下错误:

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 调用的 expressjs request.session 对象时,我注意到服务器 GET 调用具有不同的会话 ID,因此它缺少用于身份验证的令牌和令牌机密请求。如何确保客户端和服务器共享相同的会话 ID 和相同的令牌?

【问题讨论】:

    标签: node.js angular typescript express session


    【解决方案1】:

    我找不到一种方法来让它与会话一起工作,但我确实找到了一种方法来让它只使用 cookie。

    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 }
        ]
      });
    });
    

    然后,对于身份验证端点,我这样做了:

    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);
      }
    });
    

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

    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,我必须创建以下拦截器:

    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

    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 类也需要添加为提供者。

    this answer启发的解决方案。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-08
      • 2013-08-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-19
      • 1970-01-01
      • 2016-10-31
      相关资源
      最近更新 更多