Pase el parámetro a la protección de ruta

102

Estoy trabajando en una aplicación que tiene muchos roles que necesito para usar guardias para bloquear la navegación a partes de la aplicación según esos roles. Me doy cuenta de que puedo crear clases de guardia individuales para cada rol, pero prefiero tener una clase a la que de alguna manera podría pasar un parámetro. En otras palabras, me gustaría poder hacer algo similar a esto:

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

Pero como todo lo que pasa es el nombre del tipo de su guardia, no puedo pensar en una forma de hacerlo. ¿Debería simplemente tomarme la bala y escribir las clases de guardias individuales por rol y romper mi ilusión de elegancia al tener un solo tipo parametrizado en su lugar?

Brian Noyes
fuente

Respuestas:

218

En lugar de usar forRole(), puede hacer esto:

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

y usa esto en tu RoleGuard

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

    let roles = route.data.roles as Array<string>;
    ...
}
Hasan Beheshti
fuente
Gran opción también, gracias. Sin embargo, me gusta un poco mejor el enfoque del método de fábrica de Aluan, ¡pero gracias por expandir mi cerebro sobre las posibilidades!
Brian Noyes
7
Creo que la seguridad de estos datos es irrelevante. Tienes que usar autenticación y autorización en el lado del servidor. Creo que el punto de la guardia no es proteger completamente su aplicación. Si alguien lo "piratea" y navega a la página de administración, no obtendrá los datos seguros del servidor, solo verá los componentes de administración, lo cual en mi opinión está bien. Creo que esta es una solución mucho mejor que la aceptada. La solución aceptada rompe la inyección de dependencia.
bucicimaci
1
Esta es una buena solución y funciona muy bien en mi AuthGuard genérico.
SAV
3
Esta solución funciona muy bien. Mi problema es que se basa en una capa de indirección. No hay forma de que alguien que mire este código se dé cuenta de que el rolesobjeto y la protección de ruta están vinculados sin saber cómo funciona el código de antemano. Apesta que Angular no admita una forma de hacer esto de una manera más declarativa. (Para que quede claro, me estoy lamentando de que Angular no sea esta solución perfectamente razonable.)
KhalilRavanna
1
@KhalilRavanna gracias, sí, pero usé esta solución hace muchas veces y me mudé a otra solución. Mi nueva solución es un RoleGaurd y un archivo con el nombre "access.ts" con Map <URL, AccessRoles> constante, luego lo uso en RoleGaurd. Si desea controlar sus accesos en su aplicación, creo que esta nueva forma es mucho mejor, especialmente cuando tiene más de una aplicación en un proyecto.
Hasan Beheshti
11

Esta es mi opinión sobre esto y una posible solución para el problema del proveedor que falta.

En mi caso, tenemos un guardia que toma un permiso o lista de permisos como parámetro, pero es lo mismo que tiene un rol.

Tenemos una clase para tratar con los guardias de autenticación con o sin permiso:

@Injectable()
export class AuthGuardService implements CanActivate {

    checkUserLoggedIn() { ... }

Se trata de comprobar la sesión activa del usuario, etc.

También contiene un método que se utiliza para obtener un protector de permisos personalizado, que en realidad depende del AuthGuardServicepropio

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

Esto nos permite usar el método para registrar algunos guardias personalizados basados ​​en el parámetro de permisos en nuestro módulo de enrutamiento:

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

La parte interesante de forPermissiones AuthGuardService.guards.push- esto básicamente se asegura de que cada vez que forPermissionsse llama para obtener una clase de protección a medida que también lo almacenará en esta matriz. Esto también es estático en la clase principal:

public static guards = [ ]; 

Luego, podemos usar esta matriz para registrar todos los guardias; esto está bien siempre y cuando nos aseguremos de que para cuando el módulo de la aplicación registre a estos proveedores, las rutas se hayan definido y todas las clases de guardias se hayan creado (por ejemplo, verificar el orden de importación y mantenga estos proveedores lo más bajo posible en la lista; tener un módulo de enrutamiento ayuda):

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

Espero que esto ayude.

Ovidiu Dolha
fuente
1
Esta solución me da un error estático: ERROR en Error encontrado al resolver valores de símbolo de forma estática.
Arninja
Esta solución funcionó para mí para el desarrollo, pero cuando construyo la aplicación para producción en el error de lanzamientoERROR in Error during template compile of 'RoutingModule' Function calls are not supported in decorators but 'PermGuardService' was called.
kpacn
¿Funciona esto con módulos de carga diferida que tienen sus propios módulos de enrutamiento?
aplastar
2

La solución de @ AluanHaddad está dando el error "sin proveedor". Aquí hay una solución para eso (se siente sucio, pero no tengo las habilidades para hacer uno mejor).

Conceptualmente, registro, como proveedor, cada clase generada dinámicamente creada por roleGuard.

Entonces, para cada rol verificado:

canActivate: [roleGuard('foo')]

Deberías:

providers: [roleGuard('foo')]

Sin embargo, la solución de @ AluanHaddad tal como está generará una nueva clase para cada llamada a roleGuard, incluso si el rolesparámetro es el mismo. Usarlo lodash.memoizese ve así:

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

Tenga en cuenta que cada combinación de roles genera una nueva clase, por lo que debe registrarse como proveedor en cada combinación de roles. Es decir, si tienes:

canActivate: [roleGuard('foo')]y canActivate: [roleGuard('foo', 'bar')]tendrás que registrar ambos:providers[roleGuard('foo'), roleGuard('foo', 'bar')]

Una mejor solución sería registrar proveedores automáticamente en una colección de proveedores globales dentro roleGuard, pero como dije, carezco de las habilidades para implementar eso.

THX-1138
fuente
Realmente me gusta este enfoque funcional, pero la combinación de cierres con DI (clases) parece una sobrecarga.
BILL