Angular 10 SSR和expressjs会话

如何解决Angular 10 SSR和expressjs会话

我在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',res) => {
  req.session.oauthRequestToken = null;
  req.session.oauthRequestTokenSecret = null;
  res.redirect('/');
});

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

解决方法

我找不到使它与会话一起使用的方法,但是我确实找到了一种仅使用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 }
    ]
  });
});

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

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

const router = Router();

router.get('/auth/connect',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',cookieOptions);
      res.cookie('oauthRequestTokenSecret',cookieOptions);
      res.redirect(`https://twitter.com/oauth/authorize?oauth_token=${oauthToken}`);
    }
  });
});

router.get('/auth/disconnect',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',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',cookieOptions);
        res.cookie('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启发的解决方案。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-