отправить токен на сервер в угловой универсальной

У меня есть приложение Angular 6+, которое настроено на использование рендеринга на стороне сервера с использованием Angular Universal.
Я также использовал TransferState, чтобы избежать дублирования вызовов API на сервере и в клиентском приложении.

Аутентификация в моем приложении angular основана на токене.

Проблема заключается в том, что пользователь впервые открывает мое веб-приложение, что приводит к отображению index.html для пользователя, который не прошел проверку подлинности, тогда как пользователь фактически вошел в систему, но нет возможности передать токен на сервер. Таким образом, когда клиентское приложение заменяется серверным приложением, необходимо снова вызывать API из-за наличие токена в localStorage / sessionStorage браузера.

Я использовал node.js и express.js для реализации рендеринга на стороне сервера.

Я думаю, что решение - использовать сеанс и файлы cookie. Это требует от меня много работы, так как я не знаком с node.js для обработки сеансов / файлов cookie. Есть ли быстрое и простое решение?


person Fartab    schedule 01.09.2018    source источник


Ответы (1)


Для других, сталкивающихся с той же проблемой, вот решение.

клиентское приложение должно сохранять данные о состоянии, необходимые для рендеринга на стороне сервера (например, информацию для аутентификации), в файлах cookie браузера. Браузер автоматически отправляет файлы cookie в заголовке первого запроса на получение index.html. Затем в server.js мы должны извлечь cookie из заголовка запроса и передать его серверному приложению, используя extraProviders из renderModuleFactory.

Первое, что нам нужно, это сервис для работы с куки-файлами браузера. Я объявил, что один вдохновлен этим сообщением (ссылка на репозиторий github)

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*([^;]*).*$)|^.*$'), '$1'
            )
        ) || 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*(?:\=[^;]*)?(?:\1|$)/g, ''
        ).split(/\s*(?:\=[^;]*)?;\s*/);
        for (let nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
            aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
        }
        return aKeys;
    }
}

Затем мы сохраняем данные (которые мы хотим передать в серверное приложение) в 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 извлеките токен и передайте его серверному приложению:

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]},
]



Вот браузер-заглушка, окно и документ, которые я использовал:

@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 = '';
}
person Fartab    schedule 08.09.2018
comment
вы могли бы показать импорт, чтобы было проще использовать ваш код. - person Chukwuemeka Ihedoro; 30.03.2019
comment
Есть ли способ избежать файлов cookie? - person BruneX; 10.02.2020