如何解决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 举报,一经查实,本站将立刻删除。