Redireccionamiento angular a la página de inicio de sesión

122

Vengo del mundo Asp.Net MVC donde los usuarios que intentan acceder a una página que no están autorizados son redirigidos automáticamente a la página de inicio de sesión.

Estoy tratando de reproducir este comportamiento en Angular. Me encontré con el decorador @CanActivate, pero el componente no se representa en absoluto, no hay redirección.

Mi pregunta es la siguiente:

  • ¿Angular proporciona una forma de lograr este comportamiento?
  • ¿Si es así, cómo? ¿Es una buena práctica?
  • Si no es así, ¿cuál sería la mejor práctica para manejar la autorización del usuario en Angular?
Amaury
fuente
Agregué una directiva real que muestra cómo hacer las cosas de autenticación, si le interesa mirar.
Michael Oryl
Esta respuesta puede ser útil: stackoverflow.com/a/59008239/7059557
AmirReza-Farahlagha

Respuestas:

86

Actualización: publiqué un proyecto esqueleto completo de Angular 2 con integración OAuth2 en Github que muestra la directiva mencionada a continuación en acción.

Una forma de hacerlo sería mediante el uso de un directive. A diferencia de Angular 2 components, que son básicamente etiquetas HTML nuevas (con código asociado) que inserta en su página, una directiva atributiva es un atributo que coloca en una etiqueta que hace que ocurra algún comportamiento. Documentos aquí .

La presencia de su atributo personalizado hace que sucedan cosas en el componente (o elemento HTML) en el que colocó la directiva. Considere esta directiva que uso para mi aplicación Angular2 / OAuth2 actual:

import {Directive, OnDestroy} from 'angular2/core';
import {AuthService} from '../services/auth.service';
import {ROUTER_DIRECTIVES, Router, Location} from "angular2/router";

@Directive({
    selector: '[protected]'
})
export class ProtectedDirective implements OnDestroy {
    private sub:any = null;

    constructor(private authService:AuthService, private router:Router, private location:Location) {
        if (!authService.isAuthenticated()) {
            this.location.replaceState('/'); // clears browser history so they can't navigate with back button
            this.router.navigate(['PublicPage']);
        }

        this.sub = this.authService.subscribe((val) => {
            if (!val.authenticated) {
                this.location.replaceState('/'); // clears browser history so they can't navigate with back button
                this.router.navigate(['LoggedoutPage']); // tells them they've been logged out (somehow)
            }
        });
    }

    ngOnDestroy() {
        if (this.sub != null) {
            this.sub.unsubscribe();
        }
    }
}

Esto hace uso de un servicio de autenticación que escribí para determinar si el usuario ya inició sesión o no y también se suscribe al evento de autenticación para que pueda expulsar a un usuario si cierra sesión o se agota el tiempo de espera.

Podrías hacer lo mismo. Crearía una directiva como la mía que verifica la presencia de una cookie necesaria u otra información de estado que indique que el usuario está autenticado. Si no tienen esas banderas que está buscando, redirija al usuario a su página pública principal (como yo) o su servidor OAuth2 (o lo que sea). Pondría ese atributo de directiva en cualquier componente que necesite protección. En este caso, podría llamarse protectedcomo en la directiva que pegué anteriormente.

<members-only-info [protected]></members-only-info>

Luego, querrá navegar / redirigir al usuario a una vista de inicio de sesión dentro de su aplicación y manejar la autenticación allí. Tendría que cambiar la ruta actual por la que deseaba hacer. Entonces, en ese caso, usaría la inyección de dependencia para obtener un objeto Router en la constructor()función de su directiva y luego usaría el navigate()método para enviar al usuario a su página de inicio de sesión (como en mi ejemplo anterior).

Esto supone que tiene una serie de rutas en algún lugar controlando una <router-outlet>etiqueta que se parece a esto, tal vez:

@RouteConfig([
    {path: '/loggedout', name: 'LoggedoutPage', component: LoggedoutPageComponent, useAsDefault: true},
    {path: '/public', name: 'PublicPage', component: PublicPageComponent},
    {path: '/protected', name: 'ProtectedPage', component: ProtectedPageComponent}
])

Si, en cambio, necesitara redirigir al usuario a una URL externa , como su servidor OAuth2, entonces su directiva haría algo como lo siguiente:

window.location.href="https://myserver.com/oauth2/authorize?redirect_uri=http://myAppServer.com/myAngular2App/callback&response_type=code&client_id=clientId&scope=my_scope
Michael Oryl
fuente
4
¡Funciona! ¡Gracias! También encontré otro método aquí: github.com/auth0/angular2-authentication-sample/blob/master/src/… No puedo decir qué método es mejor, pero tal vez a alguien también le resulte útil.
Sergey
3
Gracias ! También agregué una nueva ruta que contiene un parámetro / protected /: returnUrl, siendo returnUrl el location.path () interceptado en ngOnInit de la directiva. Esto permite navegar al usuario después de iniciar sesión en la URL solicitada originalmente.
Amaury
1
Vea las respuestas a continuación para obtener una solución simple. Cualquier cosa tan común (redireccionar si no está autenticado) debería tener una solución simple con una respuesta de una sola oración.
Rick O'Shea
7
Nota: Esta respuesta aborda una versión beta o candidata a lanzamiento de Angular 2 y ya no es aplicable para Angular 2 final.
jbandi
1
Hay una solución mucho mejor para esto ahora usando Angular Guards
mwilson
116

Aquí hay un ejemplo actualizado usando Angular 4 (también compatible con Angular 5-8)

Rutas con ruta local protegida por AuthGuard

import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login/index';
import { HomeComponent } from './home/index';
import { AuthGuard } from './_guards/index';

const appRoutes: Routes = [
    { path: 'login', component: LoginComponent },

    // home route protected by auth guard
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

export const routing = RouterModule.forRoot(appRoutes);

AuthGuard redirige a la página de inicio de sesión si el usuario no ha iniciado sesión

Actualizado para pasar la URL original en los parámetros de consulta a la página de inicio de sesión

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private router: Router) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        if (localStorage.getItem('currentUser')) {
            // logged in so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
        return false;
    }
}

Para ver el ejemplo completo y la demostración funcional, puede consultar esta publicación

Jason
fuente
6
Tengo una Q de seguimiento, ¿no sería posible acceder a la ruta protegida si estableciera un valor arbitrario currentUseren el localStorage? p.ej. localStorage.setItem('currentUser', 'dddddd')?
jsd
2
Evitaría la seguridad del lado del cliente. Pero también eliminaría el token que sería necesario para las transacciones del lado del servidor, por lo que no se podrían extraer datos útiles de la aplicación.
Matt Meng
55

Uso con el enrutador final

Con la introducción del nuevo enrutador se hizo más fácil proteger las rutas. Debe definir un guardia, que actúa como un servicio, y agregarlo a la ruta.

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { UserService } from '../../auth';

@Injectable()
export class LoggedInGuard implements CanActivate {
  constructor(user: UserService) {
    this._user = user;
  }

  canActivate() {
    return this._user.isLoggedIn();
  }
}

Ahora pase el LoggedInGuarda la ruta y también agréguelo a la providersmatriz del módulo.

import { LoginComponent } from './components/login.component';
import { HomeComponent } from './components/home.component';
import { LoggedInGuard } from './guards/loggedin.guard';

const routes = [
    { path: '', component: HomeComponent, canActivate: [LoggedInGuard] },
    { path: 'login', component: LoginComponent },
];

La declaración del módulo:

@NgModule({
  declarations: [AppComponent, HomeComponent, LoginComponent]
  imports: [HttpModule, BrowserModule, RouterModule.forRoot(routes)],
  providers: [UserService, LoggedInGuard],
  bootstrap: [AppComponent]
})
class AppModule {}

Publicación de blog detallada sobre cómo funciona con la versión final: https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

Uso con el enrutador obsoleto

Una solución más sólida es extender el RouterOutlety al activar una ruta, verifique si el usuario está conectado. De esta manera, no tiene que copiar y pegar su directiva en cada componente. Además, la redirección basada en un subcomponente puede ser engañosa.

@Directive({
  selector: 'router-outlet'
})
export class LoggedInRouterOutlet extends RouterOutlet {
  publicRoutes: Array;
  private parentRouter: Router;
  private userService: UserService;

  constructor(
    _elementRef: ElementRef, _loader: DynamicComponentLoader,
    _parentRouter: Router, @Attribute('name') nameAttr: string,
    userService: UserService
  ) {
    super(_elementRef, _loader, _parentRouter, nameAttr);

    this.parentRouter = _parentRouter;
    this.userService = userService;
    this.publicRoutes = [
      '', 'login', 'signup'
    ];
  }

  activate(instruction: ComponentInstruction) {
    if (this._canActivate(instruction.urlPath)) {
      return super.activate(instruction);
    }

    this.parentRouter.navigate(['Login']);
  }

  _canActivate(url) {
    return this.publicRoutes.indexOf(url) !== -1 || this.userService.isLoggedIn()
  }
}

los UserService representa el lugar donde reside la lógica de negocio si el usuario está conectado o no. Puede agregarlo fácilmente con DI en el constructor.

Cuando el usuario navega a una nueva URL en su sitio web, se llama al método de activación con la instrucción actual. Desde allí, puede tomar la URL y decidir si está permitida o no. Si no, simplemente redirija a la página de inicio de sesión.

Una última cosa que queda para que funcione, es pasarlo a nuestro componente principal en lugar del integrado.

@Component({
  selector: 'app',
  directives: [LoggedInRouterOutlet],
  template: template
})
@RouteConfig(...)
export class AppComponent { }

Esta solución no se puede utilizar con el @CanActivedecorador de ciclo de vida, porque si la función que se le pasa se resuelve como falso, el método de activación delRouterOutlet no se llamará al .

También escribí una publicación de blog detallada al respecto: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492

Blacksonic
fuente
2
También escribí una publicación de blog más detallada al respecto medium.com/@blacksonic86/…
Blacksonic
Hola @Blacksonic. Acabo de comenzar a profundizar en ng2. Seguí tu sugerencia pero terminé obteniendo este error durante gulp-tslint: Failed to lint <classname>.router-outlet.ts[15,28]. In the constructor of class "LoggedInRouterOutlet", the parameter "nameAttr" uses the @Attribute decorator, which is considered as a bad practice. Please, consider construction of type "@Input() nameAttr: string". No pude averiguar qué cambiar en el constructor ("_parentRounter") para deshacerme de este mensaje. ¿Alguna idea?
leovrf
la declaración se copia del objeto integrado subyacente RouterOutlet para tener la misma firma que la clase extendida, deshabilitaría la regla tslint específica para esta línea
Blacksonic
Encontré una referencia en mgechev style-guide (busque "Preferir entradas sobre el decorador de parámetros @Attribute"). Se cambió la línea _parentRouter: Router, @Input() nameAttr: string,ay tslint ya no genera el error. También reemplazó la importación de "Atributo" a "Entrada" desde el núcleo angular. Espero que esto ayude.
leovrf
1
Hay un problema con 2.0.0-rc.1 porque RouterOutlet no se exporta y no hay posibilidad de extenderlo
mkuligowski
53

¡Por favor, no anule la salida del enrutador! Es una pesadilla con la última versión del enrutador (3.0 beta).

En su lugar, utilice las interfaces CanActivate y CanDeactivate y configure la clase como canActivate / canDeactivate en su definición de ruta.

Como eso:

{ path: '', component: Component, canActivate: [AuthGuard] },

Clase:

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(protected router: Router, protected authService: AuthService)
    {

    }

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

        if (state.url !== '/login' && !this.authService.isAuthenticated()) {
            this.router.navigate(['/login']);
            return false;
        }

        return true;
    }
}

Véase también: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Nilz11
fuente
2
Agradable, la respuesta de @ Blacksonic me funcionó perfectamente con el enrutador obsoleto. Tuve que refactorizar mucho después de actualizar al nuevo enrutador. ¡Tu solución es justo lo que necesitaba!
evandongen
No puedo hacer que canActivate funcione en el componente de mi aplicación. Estoy buscando redirigir al usuario si no está autenticado. Esta es la versión del enrutador que tengo (si necesito actualizarlo, ¿cómo lo hago usando la línea de comando git bash?) Versión que tengo: "@ angular / enrutador": "2.0.0-rc.1"
AngularM
¿Puedo usar la misma clase (AuthGuard) para proteger la ruta de otro componente?
tsiro
4

Siguiendo las impresionantes respuestas anteriores, también me gustaría CanActivateChild: proteger las rutas de los niños. Se puede usar para agregar guarda los niños rutas útiles para casos como ACL

Dice así

src / app / auth-guard.service.ts (extracto)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router:     Router) {}

  canActivate(route: ActivatedRouteSnapshot, state:    RouterStateSnapshot): boolean {
    let url: string = state.url;
    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state:  RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

/* . . . */
}

src / app / admin / admin-routing.module.ts (extracto)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

Esto está tomado de https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Thabung
fuente
2

Consulte este código, archivo auth.ts

import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';
import {  } from 'angular-2-local-storage';
import { Router } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(public localStorageService:LocalStorageService, private router: Router){}
canActivate() {
// Imaginary method that is supposed to validate an auth token
// and return a boolean
var logInStatus         =   this.localStorageService.get('logInStatus');
if(logInStatus == 1){
    console.log('****** log in status 1*****')
    return true;
}else{
    console.log('****** log in status not 1 *****')
    this.router.navigate(['/']);
    return false;
}


}

}
// *****And the app.routes.ts file is as follow ******//
      import {  Routes  } from '@angular/router';
      import {  HomePageComponent   } from './home-page/home- page.component';
      import {  WatchComponent  } from './watch/watch.component';
      import {  TeachersPageComponent   } from './teachers-page/teachers-page.component';
      import {  UserDashboardComponent  } from './user-dashboard/user- dashboard.component';
      import {  FormOneComponent    } from './form-one/form-one.component';
      import {  FormTwoComponent    } from './form-two/form-two.component';
      import {  AuthGuard   } from './authguard';
      import {  LoginDetailsComponent } from './login-details/login-details.component';
      import {  TransactionResolver } from './trans.resolver'
      export const routes:Routes    =   [
    { path:'',              component:HomePageComponent                                                 },
    { path:'watch',         component:WatchComponent                                                },
    { path:'teachers',      component:TeachersPageComponent                                         },
    { path:'dashboard',     component:UserDashboardComponent,       canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'formone',       component:FormOneComponent,                 canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'formtwo',       component:FormTwoComponent,                 canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'login-details', component:LoginDetailsComponent,            canActivate: [AuthGuard]    },

]; 
sojan
fuente
1

1. Create a guard as seen below. 2. Install ngx-cookie-service to get cookies returned by external SSO. 3. Create ssoPath in environment.ts (SSO Login redirection). 4. Get the state.url and use encodeURIComponent.

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from 
  '@angular/router';
import { CookieService } from 'ngx-cookie-service';
import { environment } from '../../../environments/environment.prod';

@Injectable()
export class AuthGuardService implements CanActivate {
  private returnUrl: string;
  constructor(private _router: Router, private cookie: CookieService) {}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.cookie.get('MasterSignOn')) {
      return true;
    } else {
      let uri = window.location.origin + '/#' + state.url;
      this.returnUrl = encodeURIComponent(uri);      
      window.location.href = environment.ssoPath +  this.returnUrl ;   
      return false;      
    }
  }
}
M.Laida
fuente