autenticação - Angular redirecionar para a página de login
Eu venho do mundo do Asp.Net MVC, onde usuários tentando acessar uma página para a qual não estão autorizados são automaticamente redirecionados para a página de login.
Estou tentando reproduzir esse comportamento no Angular. Me deparei com o decorator @CanActivate, mas ele faz com que o componente não seja renderizado de forma alguma, sem redirecionamento.
Minha pergunta é a seguinte:
- O Angular fornece uma maneira de alcançar esse comportamento?
- Se sim, como? É uma boa prática?
- Caso contrário, qual seria a melhor prática para lidar com a autorização de usuários no Angular?
Resposta #1
Aqui está um exemplo atualizado usando Angular 4 (também compatível com Angular 5 - 8)
Rotas com a rota inicial 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);
O AuthGuard redireciona para a página de login se o usuário não estiver logado
Atualizado para passar a 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 funcionando, você pode conferir este post
Resposta #2
Atualização: Eu publiquei um projeto Angular 2 completo com integração OAuth2 no Github que mostra a diretiva mencionada abaixo em ação.
Uma maneira de fazer isso seria através do uso de uma directive. Diferente dos components do Angular 2, que são basicamente novas tags HTML (com código associado) que você insere na sua página, uma diretiva atributiva é um atributo que você coloca em uma tag que faz com que algum comportamento ocorra. Documentação aqui.
A presença do seu atributo personalizado faz com que coisas aconteçam com o componente (ou elemento HTML) em que você colocou a diretiva. Considere esta diretiva que uso na minha aplicação atual Angular2/OAuth2:
import {Directive, OnDestroy} from 'angular2/core';
import {AuthService} from '../services/auth.service';
import {ROUTER_DIRECTIVES, Router, Location} from "angular2/router";
@Directive({
selector: '['
})
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 eu escrevi para determinar se o usuário já está logado e também se inscreve no evento de autenticação para que possa expulsar um usuário se ele ou ela fizer logout ou a sessão expirar.
Você poderia fazer a mesma coisa. Você criaria uma diretiva como a minha que verifica a presença de um cookie necessário ou outra informação de estado que indique que o usuário está autenticado. Se eles não tiverem esses sinais que você está procurando, redirecione o usuário para sua página pública principal (como eu faço) ou para seu servidor OAuth2 (ou qualquer outro). Você colocaria esse atributo de diretiva em qualquer componente que precise ser protegido. Neste caso, poderia ser chamado protected, como na diretiva que eu colei acima.
<members-only-info [></members-only-info>
Então você iria querer navegar/redirecionar o usuário para uma tela de login dentro do seu app, e lidar com a autenticação lá. Você teria que mudar a rota atual para a que você queria. Então, nesse caso, você usaria injeção de dependência para obter um objeto Router na função constructor() da sua diretiva e, em seguida, usar o método navigate() para enviar o usuário para a sua página de login (como no meu exemplo acima).
Isso assume que você tem uma série de rotas em algum lugar controlando uma tag <router-outlet> que se parece com algo assim, 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 uma URL externa, como seu servidor OAuth2, então você faria com que sua diretiva fizesse 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
Resposta #3
Uso com o roteador final
Com a introdução do novo roteador, tornou-se 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 LoggedInGuard para a rota e também adicione-o ao array providers 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 {}
Post de blog detalhado 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 o RouterOutlet e, ao ativar uma rota, verificar se o usuário está logado. Dessa forma, você não precisa copiar e colar sua diretiva em cada componente. Além disso, redirecionar 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()
}
}
O UserService representa o lugar onde sua lógica de negócios reside, quer o usuário esteja logado ou não. Você pode adicioná-lo facilmente com DI no construtor.
Quando o usuário navega para uma nova URL no seu site, o método activate é chamado com a Instrução atual. A partir dela, você pode pegar a URL e decidir se ela é permitida ou não. Caso não seja, basta redirecionar para a página de login.
Uma última coisa restante para fazê-lo funcionar é passá-lo para o nosso componente principal em vez do incorporado.
@Component({
selector: 'app',
directives: [LoggedInRouterOutlet],
template: template
})
@RouteConfig(...)
export class AppComponent { }
Esta solução não pode ser usada com o decorator de ciclo de vida @CanActive, porque se a função passada para ele retornar falso, o método activate do RouterOutlet não será chamado.
Também escrevi um post detalhado no blog sobre isso: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492
Resposta #4
Por favor, não substitua o Router Outlet! É um pesadelo com a última versão 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.
Assim:
{ 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;
}
}
Veja também: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard
