Как обрабатывать специфичные для запроса / Session-данные в LoopBack4 - PullRequest
0 голосов
/ 07 февраля 2020

В настоящее время я застрял с проблемой в нашем приложении LoopBack4. У нас есть несколько контроллеров. Мы используем JWT для авторизации. Внутри полезной нагрузки токенов мы храним список прав, предоставленных запрашивающему пользователю. Кроме того, мы добавили AuthorizationInterceptor для проверки прав доступа.

Я допустил ошибку, записав данные токена в переменную stati c и запросив их у служб и других мест внутри моего приложения. Если поступают параллельные запросы, один запрос перезаписывает токен другого запроса. Запрос A теперь работает с правами запроса B.

Проблема:

  • Клиент делает запрос A к приложению LB4, содержащему токен
  • Приложение сохраняет токен в переменной c stati
  • В то же время входящий запрос B передает другой токен
  • Приложение переписывает токен запроса A с помощью токен запроса B
  • Запрос A работает с правами запроса B

Приложение:

Каждый контроллер:

export class MiscController
{
    constructor(@inject(AServiceBindings.VALUE) public aService: AService) {}

    @get('/hasright', {})
    @authenticate('jwt', {"required":[1,2,3]}) // this gets checked by AuthorizationInterceptor
    async getVersion(): Promise<object>
    {
        return {hasRight: JWTService.checkRight(4)};
    }
}

jwt-service:

export class JWTService implements TokenService
{
    static AuthToken: Authtoken|null;
    static rights: number[];

    // constructor ...

    /** A method to check rights */
    static hasRight(rightId: number): boolean
    {
        return inArray(rightId, JWTService.rights);
    }

    async verifyToken(token: string): Promise<UserProfile>
    {
        // verify the token ...

        // write the Tokendata to static variables
        JWTService.AuthToken = authtoken;
        JWTService.rights = rightIds;

        return userProfile;
    }
}

export const JWTServiceBindings = {
    VALUE: BindingKey.create<JWTService>("services.JWTService")
};

AuthorizeInterceptor.ts

@globalInterceptor('', {tags: {name: 'authorize'}})
export class AuthorizationInterceptor implements Provider<Interceptor>
{
    constructor(
        @inject(AuthenticationBindings.METADATA) public metadata: AuthenticationMetadata,
        @inject(TokenServiceBindings.USER_PERMISSIONS) protected checkPermissions: UserPermissionsFn,
        @inject.getter(AuthenticationBindings.CURRENT_USER) public getCurrentUser: Getter<MyUserProfile>
    ) {}

    /**
     * This method is used by LoopBack context to produce an interceptor function
     * for the binding.
     *
     * @returns An interceptor function
     */
    value()
    {
        return this.intercept.bind(this);
    }

    /**
     * The logic to intercept an invocation
     * @param invocationCtx - Invocation context
     * @param next - A function to invoke next interceptor or the target method
     */
    async intercept(invocationCtx: InvocationContext, next: () => ValueOrPromise<InvocationResult>)
    {
        if(!this.metadata)
        {
            return next();
        }

        const requiredPermissions = this.metadata.options as RequiredPermissions;
        const user                = await this.getCurrentUser();

        if(!this.checkPermissions(user.permissions, requiredPermissions))
        {
            throw new HttpErrors.Forbidden('Permission denied! You do not have the needed right to request this function.');
        }

        return next();
    }
}

JWTAuthenticationStrategy

export class JWTAuthenticationStrategy implements AuthenticationStrategy
{
    name = 'jwt';

    constructor(@inject(JWTServiceBindings.VALUE) public tokenService: JWTService) {}

    async authenticate(request: Request): Promise<UserProfile | undefined>
    {
        const token: string = this.extractCredentials(request);

        return this.tokenService.verifyToken(token);
    }

    // extract credentials etc ...
}

application.ts

export class MyApplication extends BootMixin(ServiceMixin(RepositoryMixin(RestApplication)))
{
    constructor(options: ApplicationConfig = {})
    {
        super(options);

        // Bind authentication component related elements
        this.component(AuthenticationComponent);

        registerAuthenticationStrategy(this, JWTAuthenticationStrategy);
        this.bind(JWTServiceBindings.VALUE).toClass(JWTService);
        this.bind(TokenServiceBindings.USER_PERMISSIONS).toProvider(UserPermissionsProvider);
        this.bind(TokenServiceBindings.TOKEN_SECRET).to(TokenServiceConstants.TOKEN_SECRET_VALUE);

        // Set up the custom sequence
        this.sequence(MySequence);

        // many more bindings and other stuff to do ...
    }
}

sequence.ts

export class MySequence implements SequenceHandler
{
    // constructor ...

    async handle(context: RequestContext)
    {
        // const session = this.restoreSession(context); // restoreSession is not a function.

        try
        {
            const {request, response} = context;

            const route = this.findRoute(request);

            // call authentication action
            await this.authenticateRequest(request);
            userId = getMyUserId(); // using helper method

            // Authentication successful, proceed to invoke controller
            const args   = await this.parseParams(request, route);
            const result = await this.invoke(route, args);
            this.send(response, result);
        }
        catch(err)
        {
            this.reject(context, err);
        }
        finally
        {
            // some action using userId p.e. ...
        }
    }
}

helper.ts // простой файл, включающий небольшие функции

export function getMyUserId(): number
{
    return ((JWTService.AuthToken && JWTService.AuthToken.UserId) || 0);
}

Кроме того, мы реализовали несколько сервисов для обработки больших вещей. Теперь мне нужно решение для доступа к данным пользователей внутри сервисов и других частей приложения, например токена авторизованного пользователя. Где и как мне разместить токен?

Я нашел ссылку на StackOverflow и GitHub: Как использовать запросы с отслеживанием состояния в Loopback 4? -> https://github.com/strongloop/loopback-next/issues/1863

В обоих объяснениях я должен добавить const session = this.restoreSession(context); к собственной последовательности. Я сделал это, но restoreSession не является функцией.

Я также нашел предложение использовать пакет express -session. Это не поможет, потому что наш клиент не может хранить куки.

1 Ответ

0 голосов
/ 12 февраля 2020

Я нашел решение на основе этой документации: https://github.com/strongloop/loopback-next/blob/607dc0a3550880437568a36f3049e1de66ec73ae/docs/site/Context.md#request -level-context-request

Что я сделал?

  1. Привязать контекстные значения внутри sequence.ts
export class MySequence implements SequenceHandler
{
    // constructor ...

    async handle(context: RequestContext)
    {
        try
        {
            const {request, response} = context;

            const route = this.findRoute(request);

            // call authentication action
            const userProfile = await this.authenticateRequest(request);
            // userId = getMyUserId(); is removed, due to the fact, that we now store the value in a bind method, see next lines
            context.bind('MY_USER_ID').to(userProfile.id); // this is the essential part. "MY_USER_ID" is a key and we bind a value to that key based on the context (request)

            // Authentication successful, proceed to invoke controller
            const args   = await this.parseParams(request, route);
            const result = await this.invoke(route, args);
            this.send(response, result);
        }
        catch(err)
        {
            this.reject(context, err);
        }
        finally
        {
            // some action using userId p.e. ...
        }
    }
}

Служба JWT должна быть очищена от моего подхода c:
export class JWTService implements TokenService
{
    // static AuthToken: Authtoken|null; // not needed anymore
    // static rights: number[]; // not needed anymore

    // constructor ...

    /** A method to check rights */
    /* this is not possible anymore
    static hasRight(rightId: number): boolean
    {
        return inArray(rightId, JWTService.rights);
    }
    */

    async verifyToken(token: string): Promise<UserProfile>
    {
        // verify the token ...

        // do not write the Tokendata to static variables
        // JWTService.AuthToken = authtoken;
        // JWTService.rights = rightIds;

        return userProfile;
    }
}
Контроллеры, которые нуждаются в данных на основе контекста, должны вводить связанное значение
export class aController
{
    constructor(
        @inject('MY_USER_ID') public authorizedUserId: number 
        // here comes the injection of the bound value from the context
    ) {}

    @get('/myuserid', {})
    @authenticate('jwt', {})
    async getVersion(): Promise<object>
    {
        return this.authorizedUserId; 
        // authorizedUserId is a variable instantiated with the constructors dependency injection
    }
}

Каждый контроллер и каждый сервис, который загружается после последовательности (jwt-сервис исключен) может внедрить связанное значение и может использовать его.

Это было немного сложно, потому что связанная документация не охватывает именно этот подход, но в конце концов он работает для меня. Если у кого-то есть лучшее решение, пожалуйста, дайте мне сейчас об этом!

...