Przekaż parametr do funkcji ochrony trasy

103

Pracuję nad aplikacją, która ma wiele ról, które muszę używać strażników, aby blokować nawigację do części aplikacji w oparciu o te role. Zdaję sobie sprawę, że mogę utworzyć indywidualne klasy ochrony dla każdej roli, ale wolałbym mieć jedną klasę, do której mógłbym w jakiś sposób przekazać parametr. Innymi słowy, chciałbym móc zrobić coś podobnego do tego:

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

Ale ponieważ wszystko, co mijasz, to nazwa typu twojego strażnika, nie możesz wymyślić sposobu, aby to zrobić. Czy powinienem po prostu ugryźć punktor i napisać poszczególne klasy strażników według roli i zburzyć iluzję elegancji, mając zamiast tego jeden sparametryzowany typ?

Brian Noyes
źródło

Odpowiedzi:

220

Zamiast używać forRole(), możesz to zrobić:

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

i użyj tego w swoim RoleGuard

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

    let roles = route.data.roles as Array<string>;
    ...
}
Hasan Beheshti
źródło
Świetna opcja, dzięki. Jednak trochę lepiej podoba mi się podejście oparte na fabrycznej metodzie Aluana, ale dziękuję za rozszerzenie mojego mózgu o możliwości!
Brian Noyes
7
Myślę, że bezpieczeństwo tych danych nie ma znaczenia. Musisz użyć uwierzytelniania i autoryzacji po stronie serwera. Myślę, że celem strażnika nie jest pełna ochrona aplikacji. Jeśli ktoś go „włamie” i przejdzie na stronę administratora, nie otrzyma bezpiecznych danych z serwera tylko po prostu zobaczy komponenty administracyjne, co moim zdaniem jest w porządku. Myślę, że jest to znacznie lepsze rozwiązanie niż przyjęte. Zaakceptowane rozwiązanie przerywa wstrzykiwanie zależności.
bucicimaci
1
To dobre rozwiązanie i świetnie działa w moim generycznym AuthGuardzie.
SAV
3
To rozwiązanie działa świetnie. Mój problem polega na tym, że opiera się na warstwie pośredniej. Nie ma możliwości, aby ktoś, kto spojrzał na ten kod, zdał sobie sprawę, że rolesobiekt i strażnik trasy są połączone, nie wiedząc wcześniej, jak działa kod. Szkoda, że ​​Angular nie obsługuje sposobu, aby to zrobić w bardziej deklaratywny sposób. (Żeby było jasne, to ja
narzekam na Angulara, a
1
@KhalilRavanna dziękuję, tak, ale korzystałem z tego rozwiązania wiele razy temu i przeniosłem się na inne rozwiązanie. Moje nowe rozwiązanie to jeden RoleGaurd i jeden plik z nazwą „access.ts” ze stałą Map <URL, AccessRoles>, a następnie używam go w RoleGaurd. Jeśli chcesz kontrolować dostęp do swojej aplikacji, myślę, że ten nowy sposób jest znacznie lepszy, zwłaszcza gdy masz więcej niż jedną aplikację w jednym projekcie.
Hasan Beheshti
11

Oto moje podejście do tego i możliwe rozwiązanie problemu z brakującym dostawcą.

W moim przypadku mamy strażnika, który jako parametr przyjmuje pozwolenie lub listę uprawnień, ale to samo ma rolę.

Mamy klasę do radzenia sobie ze strażnikami auth z pozwoleniem lub bez:

@Injectable()
export class AuthGuardService implements CanActivate {

    checkUserLoggedIn() { ... }

Dotyczy to sprawdzania aktywnej sesji użytkownika itp.

Zawiera również metodę używaną do uzyskiwania niestandardowej ochrony uprawnień, która w rzeczywistości zależy od AuthGuardServicesiebie

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;
  }

To pozwala nam użyć metody do zarejestrowania niektórych niestandardowych strażników na podstawie parametru permissions w naszym module routingu:

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

Interesującą częścią forPermissionjest AuthGuardService.guards.pushto, że w zasadzie zapewnia to, że za każdym razem, gdy forPermissionszostanie wywołana w celu uzyskania niestandardowej klasy ochrony, będzie ona również przechowywać ją w tej tablicy. Jest to również statyczne w głównej klasie:

public static guards = [ ]; 

Następnie możemy użyć tej tablicy do zarejestrowania wszystkich strażników - jest to w porządku, o ile upewnimy się, że zanim moduł aplikacji zarejestruje tych dostawców, trasy zostały zdefiniowane i wszystkie klasy ochrony zostały utworzone (np. Sprawdź kolejność importu i utrzymuj tych dostawców jak najniżej na liście - posiadanie modułu routingu pomaga):

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

Mam nadzieję że to pomoże.

Ovidiu Dolha
źródło
1
To rozwiązanie daje mi błąd statyczny: ERROR in Error napotkał statyczne rozwiązywanie wartości symboli.
Arninja
To rozwiązanie działało u mnie na development, ale kiedy buduję aplikację do produkcji wrzuca błądERROR in Error during template compile of 'RoutingModule' Function calls are not supported in decorators but 'PermGuardService' was called.
kpacn
Czy to działa z leniwie ładowanymi modułami, które mają własne moduły routingu?
zmiażdżyć
2

Rozwiązanie @ AluanHaddad wyświetla błąd „brak dostawcy”. Oto rozwiązanie tego problemu (wydaje się brudne, ale brakuje mi umiejętności, aby ulepszyć).

Koncepcyjnie rejestruję jako dostawca każdą dynamicznie generowaną klasę utworzoną przez roleGuard.

Więc dla każdej sprawdzonej roli:

canActivate: [roleGuard('foo')]

powinieneś mieć:

providers: [roleGuard('foo')]

Jednak rozwiązanie @ AluanHaddad w stanie obecnym wygeneruje nową klasę dla każdego wywołania roleGuard, nawet jeśli rolesparametr jest taki sam. Używanie lodash.memoizego wygląda następująco:

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;
        }
    }
});

Zauważ, że każda kombinacja ról generuje nową klasę, więc musisz zarejestrować jako dostawca każdą kombinację ról. To znaczy, jeśli masz:

canActivate: [roleGuard('foo')]i canActivate: [roleGuard('foo', 'bar')]będziesz musiał zarejestrować oba:providers[roleGuard('foo'), roleGuard('foo', 'bar')]

Lepszym rozwiązaniem byłoby automatyczne rejestrowanie dostawców w globalnej kolekcji dostawców wewnątrz roleGuard, ale jak powiedziałem, brakuje mi umiejętności, aby to zaimplementować.

THX-1138
źródło
Bardzo podoba mi się to funkcjonalne podejście, ale domknięcia mieszane z DI (klasami) wyglądają jak narzut.
BILL