Redirecionamento angular para a página de login


122

Eu venho do mundo Asp.Net MVC, onde os usuários que tentam acessar uma página que não estão autorizadas são automaticamente redirecionados para a página de login.

Estou tentando reproduzir esse comportamento no Angular. Eu vim pelo decorador @CanActivate, mas resulta no componente não renderizando, sem redirecionamento.

Minha pergunta é a seguinte:

  • O Angular fornece uma maneira de atingir esse comportamento?
  • Se sim, como? É uma boa prática?
  • Caso contrário, qual seria a melhor prática para lidar com a autorização do usuário no Angular?

Eu adicionei uma diretiva real que mostra como fazer as coisas de autenticação, se você quiser olhar.
Michael Oryl

Respostas:


86

Atualização: publiquei um projeto esqueleto completo do Angular 2 com integração do OAuth2 no Github que mostra a diretiva mencionada abaixo em ação.

Uma maneira de fazer isso seria através do uso de a directive. Ao contrário do Angular 2 components, que são basicamente novas tags HTML (com código associado) que você insere em sua página, uma diretiva de atributo é um atributo que você coloca em uma marca que causa algum comportamento. Documentos aqui .

A presença do seu atributo personalizado faz com que as coisas aconteçam com o componente (ou elemento HTML) em que você colocou a diretiva. Considere esta diretiva que eu uso no meu aplicativo Angular2 / OAuth2 atual:

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

Isso faz uso de um serviço de autenticação que escrevi para determinar se o usuário já está conectado ou não e também se inscreve no evento de autenticação para que ele possa expulsar um usuário se ele se desconectar ou atingir o tempo limite.

Você poderia fazer a mesma coisa. Você criaria uma diretiva como a minha que verifica a presença de um cookie necessário ou outras informações de estado que indiquem que o usuário está autenticado. Se eles não tiverem os sinalizadores que você procura, redirecione o usuário para sua página pública principal (como eu) ou para o servidor OAuth2 (ou o que for). Você colocaria esse atributo de diretiva em qualquer componente que precise ser protegido. Nesse caso, pode ser chamado protectedcomo na diretiva que colei acima.

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

Em seguida, você deseja navegar / redirecionar o usuário para uma visualização de login no seu aplicativo e manipular a autenticação lá. Você teria que mudar a rota atual para a que você queria fazer isso. Portanto, nesse caso, você usaria a injeção de dependência para obter um objeto Router na constructor()função da diretiva e, em seguida, usaria o navigate()método para enviar o usuário à sua página de login (como no meu exemplo acima).

Isso pressupõe que você tenha uma série de rotas em algum lugar controlando uma <router-outlet>tag que se parece com isso, talvez:

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

Se, em vez disso, você precisasse redirecionar o usuário para um URL externo , como o servidor OAuth2, sua diretiva faria algo como o seguinte:

window.location.href="https://myserver.com/oauth2/authorize?redirect_uri=http://myAppServer.com/myAngular2App/callback&response_type=code&client_id=clientId&scope=my_scope

4
Funciona! obrigado! Também encontrei outro método aqui - github.com/auth0/angular2-authentication-sample/blob/master/src/… Não sei dizer qual método é melhor, mas talvez alguém ache útil também.
Sergey

3
Obrigado ! Também adicionei uma nova rota contendo um parâmetro / protected /: returnUrl, returnUrl sendo o location.path () interceptado em ngOnInit da diretiva. Isso permite navegar pelo usuário após o login no URL solicitado originalmente.
Amaury

1
Veja as respostas abaixo para uma solução simples. Qualquer coisa comum (redirecionar se não autenticada) deve ter uma solução simples com uma única resposta de sentença.
Rick O'Shea

7
Nota: Esta resposta aborda uma versão beta ou candidata a lançamento do Angular 2 e não é mais aplicável à final do Angular 2.
jbandi

1
Há uma solução muito melhor para isso agora usando angular Guards
mwilson

116

Aqui está um exemplo atualizado usando Angular 4 (também compatível com Angular 5 - 8)

Rotas com rota doméstica protegidas pelo 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 redireciona para a página de login se o usuário não estiver logado

Atualizado para passar o URL original nos parâmetros de consulta para a página de login

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 o exemplo completo e a demonstração de trabalho, você pode conferir esta postagem


6
Eu tenho um acompanhamento Q, não é, se a definição de um valor arbitrário para currentUserno localStorageainda seria capaz de acessar a rota protegida? por exemplo. localStorage.setItem('currentUser', 'dddddd')?
JSD

2
Ignoraria a segurança do lado do cliente. Mas também limparia o token necessário para transações no servidor, para que nenhum dado útil pudesse ser extraído do aplicativo.
Matt Meng

55

Uso com o roteador final

Com a introdução do novo roteador, ficou mais fácil proteger as rotas. Você deve definir um guarda, que atua como um serviço, e adicioná-lo à rota.

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

Agora passe o LoggedInGuardpara a rota e também adicione-o à providersmatriz do 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 },
];

A declaração do módulo:

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

Postagem detalhada no blog sobre como funciona com a versão final: https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

Uso com o roteador obsoleto

Uma solução mais robusta é estender RouterOutlete ao ativar uma rota, verificar se o usuário está conectado. Dessa forma, você não precisa copiar e colar sua diretiva em todos os componentes. Além disso, o redirecionamento com base em um subcomponente pode ser enganoso.

@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()
  }
}

A UserServicerepresenta o lugar onde sua lógica de negócios reside se o usuário está logado ou não. Você pode adicioná-lo facilmente com DI no construtor.

Quando o usuário navega para um novo URL no seu site, o método de ativação é chamado com a Instrução atual. A partir dele, você pode pegar o URL e decidir se é permitido ou não. Se não, basta redirecionar para a página de login.

Uma última coisa que resta para fazê-lo funcionar é repassá-lo ao nosso componente principal, em vez do componente interno.

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

Esta solução não pode ser usada com o @CanActivedecorador do ciclo de vida, porque se a função passada para ele resolver falsa, o método de ativação do RouterOutletnão será chamado.

Também escreveu uma postagem detalhada no blog: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492


2
Também escreveu uma postagem de blog mais detalhada sobre isso medium.com/@blacksonic86/…
Blacksonic 7/16/16

Olá @Blacksonic. Apenas comecei a cavar no ng2. Eu segui sua sugestão, mas acabei recebendo este erro durante o 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". Não foi possível descobrir o que alterar no construtor ("_parentRounter") para se livrar dessa mensagem. Alguma ideia?
leovrf

a declaração é copiado do subjacente construído em objeto RouterOutlet ter a mesma assinatura que a classe estendida, eu iria desativar regra tslint específico para esta linha
Blacksonic

Encontrei uma referência no guia de estilo mgechev (procure "Preferências de entrada sobre o decorador de parâmetros @Attribute"). Mudou a linha para _parentRouter: Router, @Input() nameAttr: string,e tslint não gera mais o erro. Também substituiu a importação "Atributo" para "Entrada" do núcleo angular. Espero que isto ajude.
leovrf

1
Há um problema com o 2.0.0-rc.1 porque RouterOutlet não é exportado e não há possibilidade de estendê-lo
mkuligowski

53

Por favor, não substitua a saída do roteador! É um pesadelo com a versão mais recente do roteador (3.0 beta).

Em vez disso, use as interfaces CanActivate e CanDeactivate e defina a classe como canActivate / canDeactivate na sua definição de rota.

Curtiu isso:

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

Classe:

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

Consulte também: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard


2
Bom, a resposta de @ Blacksonic estava funcionando perfeitamente para mim com o roteador obsoleto. Eu tive que refatorar muito depois de atualizar para o novo roteador. Sua solução é exatamente o que eu precisava!
evandongen

Não consigo obter o canActivate para trabalhar no meu app.component. Estou procurando redirecionar o usuário se não estiver autenticado. Esta é a versão do roteador que eu tenho (se eu precisar atualizá-lo, como faço isso usando a linha de comando git bash?) Versão eu tenho: "@ angular / router": "2.0.0-rc.1"
AngularM

posso usar a mesma classe (AuthGuard) para proteger outra rota de componente?
tsiro 14/01

4

Seguindo as respostas impressionantes acima, eu também gostaria de CanActivateChild: guardar rotas para crianças. Ele pode ser usado para adicionar guardrotas secundárias úteis para casos como ACLs

É assim

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

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 (trecho)

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

Isso é obtido em https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard


2

Consulte este código, arquivo 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]    },

]; 

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;      
    }
  }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.