在 angular universal 中将令牌发送到服务器
send token to server in angular universal
我有一个 Angular 6+ 应用程序,它配置为使用 Angular Universal.
利用服务器端渲染
我还使用 TransferState 来避免在服务器和客户端应用程序上重复 API 调用。
我的 angular 应用中的身份验证基于令牌。
问题出在用户第一次打开我的网络应用程序时,这导致为未通过身份验证的用户呈现 index.html,而用户实际上已登录但没有机会转移令牌到服务器。因此,当客户端应用swapped与服务器应用时,由于浏览器localStorage/sessionStorage.
中存在令牌,因此需要再次调用APIs
我使用 node.js 和 express.js 来实现服务器端渲染。
我认为解决方案是使用会话和 cookie。这对我来说需要做很多工作,因为我不熟悉 node.js 来处理 sessions/cookies。有什么快速简便的解决方案吗?
对于面临同样问题的其他人,这里是解决方案。
client-app 应在浏览器 cookie 中保存服务器端呈现所需的状态数据(例如身份验证信息)。浏览器将在第一个请求 index.html
的 header 中自动发送 cookie。然后在 server.js
中,我们应该从请求 header 中提取 cookie,并使用 extraProviders
of renderModuleFactory
将其传递给 server-app .
我们首先需要的是处理浏览器 cookie 的服务。我宣布了一个灵感来自 this post (github repo link)
import {Injectable} from '@angular/core';
@Injectable()
export class CookieManager {
getItem(cookies, sKey): string {
if (!sKey) {
return null;
}
return decodeURIComponent(cookies.replace(new RegExp(
'(?:(?:^|.*;)\s*' +
encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, '\$&') +
'\s*\=\s*([^;]*).*$)|^.*$'), ''
)
) || null;
}
setItem(cookies, sKey, sValue, vEnd?, sPath?, sDomain?, bSecure?): string {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
return cookies;
}
let sExpires = '';
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : '; max-age=' + vEnd;
break;
case String:
sExpires = '; expires=' + vEnd;
break;
case Date:
sExpires = '; expires=' + vEnd.toUTCString();
break;
}
}
return encodeURIComponent(sKey) + '=' + encodeURIComponent(sValue) + sExpires +
(sDomain ? '; domain=' + sDomain : '') + (sPath ? '; path=' + sPath : '') + (bSecure ? '; secure' : '');
}
removeItem(cookies, sKey, sPath?, sDomain?): string {
if (!this.hasItem(cookies, sKey)) {
return cookies;
}
return encodeURIComponent(sKey) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT' +
(sDomain ? '; domain=' + sDomain : '') + (sPath ? '; path=' + sPath : '');
}
hasItem(cookies, sKey): boolean {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
return false;
}
return (new RegExp('(?:^|;\s*)' +
encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, '\$&') + '\s*\='
)).test(cookies);
}
keys(cookies) {
const aKeys = cookies.replace(
/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:|$)/g, ''
).split(/\s*(?:\=[^;]*)?;\s*/);
for (let nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
}
return aKeys;
}
}
接下来我们将数据(我们要传递给 server-app)保存在浏览器的 cookie 中
@Injectable()
export class AuthenticationService {
constructor(private http: HttpClient,
private cookieManager: CookieManager,
@Inject(BROWSER) private browser: BrowserInterface) { }
login(username: string, password: string) {
return this.http.post<any>(`${apiUrl}/users/authenticate`, { username: username, password: password })
.pipe(tap(user => {
if (user && user.token) {
// store authentication details in local storage and browser cookie
this.browser.document.localStorage.setItem('authenticatedUser', JSON.stringify(user));
this.saveInCookies('authenticatedUser', user)
}
}));
}
private saveInCookies(key, data){
const document = this.browser.document;
let cookieStorage = this.cookieManager.getItem(document.cookie, 'storage');
cookieStorage = cookieStorage ? JSON.parse(cookieStorage) : {};
cookieStorage[key] = data;
document.cookie = this.cookieManager.setItem(document.cookie, 'storage', JSON.stringify(cookieStorage));
}
}
最后在server.ts
中提取令牌并将其传递给server-app:
app.engine('html', (_, options, callback) => {
// extract request cookie
const cookieHeader = options.req.headers.cookie;
renderModuleFactory(AppServerModuleNgFactory, {
document: template,
url: options.req.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
// pass cookie using dependency injection
{provide: 'CLIENT_COOKIES', useValue: cookieHeader}
]
}).then(html => {
callback(null, html);
});
});
并在这样的服务中使用提供的 cookie:
import {Inject} from '@angular/core';
export class ServerStorage {
private clientCookies: object;
constructor(@Inject('CLIENT_COOKIES') clientCookies: string,
cookieManager: CookieManager) {
const cookieStorage = cookieManager.getItem(clientCookies, 'storage');
this.clientCookies = cookieStorage ? JSON.parse(cookieStorage) : {};
}
clear(): void {
this.clientCookies = {};
}
getItem(key: string): string | null {
return this.clientCookies[key];
}
setItem(key: string, value: string): void {
this.clientCookies[key] = value;
}
}
在 app.server.module.ts
的供应商中使用 ServerStorage
在 StubBrowser
providers: [
{provide: BROWSER, useClass: StubBrowser, deps: [ServerStorage]},
]
这是存根浏览器,window 和我使用的文档:
@Injectable()
export class StubBrowser implements BrowserInterface {
public readonly window;
constructor(localStorage: ServerStorage) {
this.window = new StubWindow(localStorage);
}
get document() {
return this.window.document;
}
get navigator() {
return this.window.navigator;
}
get localStorage() {
return this.window.localStorage;
}
}
class StubWindow {
constructor(public localStorage: ServerStorage) {
}
readonly document = new StubDocument();
readonly navigator = {userAgent: 'stub_user_agent'};
}
class StubDocument {
public cookie = '';
}
我有一个 Angular 6+ 应用程序,它配置为使用 Angular Universal.
利用服务器端渲染
我还使用 TransferState 来避免在服务器和客户端应用程序上重复 API 调用。
我的 angular 应用中的身份验证基于令牌。
问题出在用户第一次打开我的网络应用程序时,这导致为未通过身份验证的用户呈现 index.html,而用户实际上已登录但没有机会转移令牌到服务器。因此,当客户端应用swapped与服务器应用时,由于浏览器localStorage/sessionStorage.
中存在令牌,因此需要再次调用APIs我使用 node.js 和 express.js 来实现服务器端渲染。
我认为解决方案是使用会话和 cookie。这对我来说需要做很多工作,因为我不熟悉 node.js 来处理 sessions/cookies。有什么快速简便的解决方案吗?
对于面临同样问题的其他人,这里是解决方案。
client-app 应在浏览器 cookie 中保存服务器端呈现所需的状态数据(例如身份验证信息)。浏览器将在第一个请求 index.html
的 header 中自动发送 cookie。然后在 server.js
中,我们应该从请求 header 中提取 cookie,并使用 extraProviders
of renderModuleFactory
将其传递给 server-app .
我们首先需要的是处理浏览器 cookie 的服务。我宣布了一个灵感来自 this post (github repo link)
import {Injectable} from '@angular/core';
@Injectable()
export class CookieManager {
getItem(cookies, sKey): string {
if (!sKey) {
return null;
}
return decodeURIComponent(cookies.replace(new RegExp(
'(?:(?:^|.*;)\s*' +
encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, '\$&') +
'\s*\=\s*([^;]*).*$)|^.*$'), ''
)
) || null;
}
setItem(cookies, sKey, sValue, vEnd?, sPath?, sDomain?, bSecure?): string {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
return cookies;
}
let sExpires = '';
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : '; max-age=' + vEnd;
break;
case String:
sExpires = '; expires=' + vEnd;
break;
case Date:
sExpires = '; expires=' + vEnd.toUTCString();
break;
}
}
return encodeURIComponent(sKey) + '=' + encodeURIComponent(sValue) + sExpires +
(sDomain ? '; domain=' + sDomain : '') + (sPath ? '; path=' + sPath : '') + (bSecure ? '; secure' : '');
}
removeItem(cookies, sKey, sPath?, sDomain?): string {
if (!this.hasItem(cookies, sKey)) {
return cookies;
}
return encodeURIComponent(sKey) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT' +
(sDomain ? '; domain=' + sDomain : '') + (sPath ? '; path=' + sPath : '');
}
hasItem(cookies, sKey): boolean {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
return false;
}
return (new RegExp('(?:^|;\s*)' +
encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, '\$&') + '\s*\='
)).test(cookies);
}
keys(cookies) {
const aKeys = cookies.replace(
/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:|$)/g, ''
).split(/\s*(?:\=[^;]*)?;\s*/);
for (let nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
}
return aKeys;
}
}
接下来我们将数据(我们要传递给 server-app)保存在浏览器的 cookie 中
@Injectable()
export class AuthenticationService {
constructor(private http: HttpClient,
private cookieManager: CookieManager,
@Inject(BROWSER) private browser: BrowserInterface) { }
login(username: string, password: string) {
return this.http.post<any>(`${apiUrl}/users/authenticate`, { username: username, password: password })
.pipe(tap(user => {
if (user && user.token) {
// store authentication details in local storage and browser cookie
this.browser.document.localStorage.setItem('authenticatedUser', JSON.stringify(user));
this.saveInCookies('authenticatedUser', user)
}
}));
}
private saveInCookies(key, data){
const document = this.browser.document;
let cookieStorage = this.cookieManager.getItem(document.cookie, 'storage');
cookieStorage = cookieStorage ? JSON.parse(cookieStorage) : {};
cookieStorage[key] = data;
document.cookie = this.cookieManager.setItem(document.cookie, 'storage', JSON.stringify(cookieStorage));
}
}
最后在server.ts
中提取令牌并将其传递给server-app:
app.engine('html', (_, options, callback) => {
// extract request cookie
const cookieHeader = options.req.headers.cookie;
renderModuleFactory(AppServerModuleNgFactory, {
document: template,
url: options.req.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
// pass cookie using dependency injection
{provide: 'CLIENT_COOKIES', useValue: cookieHeader}
]
}).then(html => {
callback(null, html);
});
});
并在这样的服务中使用提供的 cookie:
import {Inject} from '@angular/core';
export class ServerStorage {
private clientCookies: object;
constructor(@Inject('CLIENT_COOKIES') clientCookies: string,
cookieManager: CookieManager) {
const cookieStorage = cookieManager.getItem(clientCookies, 'storage');
this.clientCookies = cookieStorage ? JSON.parse(cookieStorage) : {};
}
clear(): void {
this.clientCookies = {};
}
getItem(key: string): string | null {
return this.clientCookies[key];
}
setItem(key: string, value: string): void {
this.clientCookies[key] = value;
}
}
在 app.server.module.ts
的供应商中使用 ServerStorage
在 StubBrowser
providers: [
{provide: BROWSER, useClass: StubBrowser, deps: [ServerStorage]},
]
这是存根浏览器,window 和我使用的文档:
@Injectable()
export class StubBrowser implements BrowserInterface {
public readonly window;
constructor(localStorage: ServerStorage) {
this.window = new StubWindow(localStorage);
}
get document() {
return this.window.document;
}
get navigator() {
return this.window.navigator;
}
get localStorage() {
return this.window.localStorage;
}
}
class StubWindow {
constructor(public localStorage: ServerStorage) {
}
readonly document = new StubDocument();
readonly navigator = {userAgent: 'stub_user_agent'};
}
class StubDocument {
public cookie = '';
}