Передать параметр в защиту маршрута

103

Я работаю над приложением, у которого есть много ролей, которые мне нужно использовать для блокировки навигации к частям приложения на основе этих ролей. Я понимаю, что могу создавать отдельные классы защиты для каждой роли, но предпочитаю иметь один класс, в который я мог бы каким-то образом передать параметр. Другими словами, я хотел бы сделать что-то подобное:

{ 
  path: 'super-user-stuff', 
  component: SuperUserStuffComponent,
  canActivate: [RoleGuard.forRole('superUser')]
}

Но поскольку все, что вы передаете, - это имя типа вашего охранника, не могу придумать, как это сделать. Должен ли я просто укусить пулю и написать отдельные классы защиты для каждой роли и разрушить мою иллюзию элегантности, имея вместо этого один параметризованный тип?

Брайан Нойес
источник

Ответы:

220

Вместо использования forRole()вы можете сделать это:

{ 
   path: 'super-user-stuff', 
   component: SuperUserStuffComponent,
   canActivate: RoleGuard,
   data: {roles: ['SuperAdmin', ...]}
}

и используйте это в своем RoleGuard

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
    : Observable<boolean> | Promise<boolean> | boolean  {

    let roles = route.data.roles as Array<string>;
    ...
}
Хасан Бехешти
источник
Отличный вариант, спасибо. Мне нравится подход Алуана к фабричному методу, только трассировка немного лучше, но спасибо за то, что расширил мой мозг над возможностями!
Брайан Нойес
7
Я думаю, что безопасность этих данных не имеет значения. Вы должны использовать аутентификацию и авторизацию на стороне сервера. Я думаю, что суть охраны не в том, чтобы полностью защитить ваше приложение. Если кто-то «взломает» его и перейдет на страницу администратора, он / она не получит защищенные данные с сервера, а просто увидит ваши компоненты администратора, что, на мой взгляд, нормально. Думаю, это гораздо лучшее решение, чем принятое. Принятое решение прерывает внедрение зависимостей.
bucicimaci
1
Это хорошее решение, и оно отлично работает в моем общем AuthGuard.
SAV
3
Это решение отлично работает. Моя проблема в том, что он основан на косвенном обращении. Невозможно, чтобы кто-то, глядя на этот код, понял, что rolesобъект и средство защиты маршрута связаны, не зная заранее, как работает код. Отстойно, что Angular не поддерживает способ сделать это более декларативным способом. (Чтобы прояснить, я оплакиваю Angular, а не это совершенно разумное решение.)
Халил Раванна,
1
@KhalilRavanna, спасибо, да, но я использовал это решение много раз назад и перешел на другое решение. Мое новое решение - это один RoleGaurd и один файл с именем «access.ts» с константой Map <URL, AccessRoles> в нем, затем я использую его в RoleGaurd. Если вы хотите контролировать доступ в своем приложении, я думаю, что этот новый способ намного лучше, особенно когда у вас более одного приложения в одном проекте.
Хасан Бехешти
11

Вот мой взгляд на это и возможное решение проблемы с отсутствующим поставщиком.

В моем случае у нас есть охранник, который принимает разрешение или список разрешений в качестве параметра, но это то же самое, что имеет роль.

У нас есть класс для работы с охранниками авторизации с разрешения или без разрешения:

@Injectable()
export class AuthGuardService implements CanActivate {

    checkUserLoggedIn() { ... }

Это касается проверки активного сеанса пользователя и т. Д.

Он также содержит метод, используемый для получения настраиваемой защиты разрешений, которая фактически зависит от AuthGuardServiceсамого

static forPermissions(permissions: string | string[]) {
    @Injectable()
    class AuthGuardServiceWithPermissions {
      constructor(private authGuardService: AuthGuardService) { } // uses the parent class instance actually, but could in theory take any other deps

      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        // checks typical activation (auth) + custom permissions
        return this.authGuardService.canActivate(route, state) && this.checkPermissions();
      }

      checkPermissions() {
        const user = ... // get the current user
        // checks the given permissions with the current user 
        return user.hasPermissions(permissions);
      }
    }

    AuthGuardService.guards.push(AuthGuardServiceWithPermissions);
    return AuthGuardServiceWithPermissions;
  }

Это позволяет нам использовать метод для регистрации некоторых настраиваемых охранников на основе параметра разрешений в нашем модуле маршрутизации:

....
{ path: 'something', 
  component: SomeComponent, 
  canActivate: [ AuthGuardService.forPermissions('permission1', 'permission2') ] },

Интересная часть forPermissionзаключается AuthGuardService.guards.pushв том, что это в основном гарантирует, что каждый раз, когда forPermissionsвызывается для получения настраиваемого класса защиты, он также сохраняет его в этом массиве. Это также статично для основного класса:

public static guards = [ ]; 

Затем мы можем использовать этот массив для регистрации всех охранников - это нормально, если мы убедимся, что к тому времени, когда модуль приложения зарегистрирует этих поставщиков, маршруты были определены и все классы защиты были созданы (например, проверьте порядок импорта и держите этих провайдеров как можно ниже в списке - помогает наличие модуля маршрутизации):

providers: [
    // ...
    AuthGuardService,
    ...AuthGuardService.guards,
]

Надеюсь это поможет.

Овидиу Долха
источник
1
Это решение дает мне статическую ошибку: ERROR in Error обнаружила статическое разрешение значений символов.
Arninja 07
Это решение сработало для меня при разработке, но когда я создаю приложение для производства, выдает ошибкуERROR in Error during template compile of 'RoutingModule' Function calls are not supported in decorators but 'PermGuardService' was called.
kpacn
Работает ли это с модулями с отложенной загрузкой, у которых есть собственные модули маршрутизации?
раздавить
2

Решение @AluanHaddad выдает ошибку «нет провайдера». Вот исправление для этого (это кажется грязным, но у меня нет навыков, чтобы исправить это).

По сути, я регистрирую как поставщик каждый динамически генерируемый класс, созданный с помощью roleGuard.

Итак, для каждой проверенной роли:

canActivate: [roleGuard('foo')]

у вас должно быть:

providers: [roleGuard('foo')]

Однако решение @AluanHaddad как есть будет генерировать новый класс для каждого вызова roleGuard, даже если rolesпараметр тот же. Использование lodash.memoizeэто выглядит так:

export var roleGuard = _.memoize(function forRole(...roles: string[]): Type<CanActivate> {
    return class AuthGuard implements CanActivate {
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
            Observable<boolean>
            | Promise<boolean>
            | boolean {
            console.log(`checking access for ${roles.join(', ')}.`);
            return true;
        }
    }
});

Обратите внимание, что каждая комбинация ролей генерирует новый класс, поэтому вам необходимо зарегистрировать в качестве поставщика каждую комбинацию ролей. Т.е. если у вас есть:

canActivate: [roleGuard('foo')]и canActivate: [roleGuard('foo', 'bar')]вам нужно будет зарегистрировать оба:providers[roleGuard('foo'), roleGuard('foo', 'bar')]

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

THX-1138
источник
Мне очень нравится этот функциональный подход, но замыкания миксинов с DI (классами) выглядят как накладные расходы.
БИЛЛ