角度重定向到登录页面


122

我来自Asp.Net MVC世界,在其中尝试访问未经授权的页面的用户会自动重定向到登录页面。

我正在尝试在Angular上重现此行为。我遇到了@CanActivate装饰器,但是它导致该组件完全不呈现,也没有重定向。

我的问题如下:

  • Angular是否提供实现此行为的方法?
  • 如果是这样,怎么办?这是一个好习惯吗?
  • 如果没有,在Angular中处理用户授权的最佳实践是什么?

我添加了一个实际的指令,该指令说明了如何进行身份验证,如果您愿意的话。
Michael Oryl 2015年

Answers:


86

更新:我已经在Github上发布了具有OAuth2集成的完整框架Angular 2项目,该项目显示了下面提到的指令。

一种方法是通过使用directive。与Angular 2不同,Angular 2 components基本上是您插入到页面中的新HTML标记(带有关联的代码),而归因指令是您放入标记中的属性,该属性会导致某些行为。 文档在这里

自定义属性的存在会导致放置指令的组件(或HTML元素)发生问题。考虑一下我在当前Angular2 / OAuth2应用程序中使用的该指令:

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

这利用了我编写的身份验证服务来确定用户是否已经登录并且还订阅了身份验证事件,以便它可以在用户注销或超时时将其踢出。

您可以做同样的事情。您将创建一个类似我的指令,以检查是否存在必要的cookie或其他状态信息,该信息指示用户已通过身份验证。如果它们没有您要查找的标志,则将用户重定向到您的主公共页面(如我)或您的OAuth2服务器(或任何其他地址)。您可以将该指令属性放在需要保护的任何组件上。在这种情况下,可以protected像我上面粘贴的指令中那样调用它。

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

然后,您需要将用户导航/重定向到您应用程序中的登录视图,并在那里进行身份验证。您必须将当前路线更改为您要执行的路线。因此,在那种情况下,您将使用依赖注入在指令的函数中获取一个Router对象constructor(),然后使用该navigate()方法将用户发送到登录页面(如上述示例所示)。

假设您在某个地方控制着<router-outlet>看起来像这样的标签的一系列路线,也许:

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

相反,如果您需要将用户重定向到外部 URL(例如OAuth2服务器),那么您的指令将执行以下操作:

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
有用!谢谢!我还在这里找到了另一种方法-github.com/auth0/angular2-authentication-sample/blob/master/src / ...我不能说哪种方法更好,但是也许有人会发现它也很有用。
谢尔盖

3
谢谢 !我还添加了一条包含参数/ protected /:returnUrl的新路由,returnUrl是在指令的ngOnInit处截取的location.path()。这允许在登录到最初提示的URL后导航用户。
阿毛里

1
请参阅下面的答案以获取简单的解决方案。任何常见的问题(如果未通过身份验证,都将重定向)应该有一个简单的解决方案,只需一个句子即可解决。
瑞克·奥谢

7
注意:此答案针对的是Angular 2的Beta版或候选版本,并且不再适用于Angular 2 final。
jbandi

1
现在使用Angular Guards可以更好地解决此问题
mwilson

116

这是使用Angular 4的更新示例(也与Angular 5-8兼容)

带有由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重定向到登录页面

更新为将查询参数中的原始url传递到登录页面

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

有关完整的示例和可行的演示,您可以查看这篇文章


6
我有一个跟进Q,是不是如果设置为任意值currentUserlocalStorage仍然能够访问受保护的途径?例如。localStorage.setItem('currentUser', 'dddddd')
jsd

2
它将绕过客户端安全性。但这也会清除服务器端事务所需的令牌,因此无法从应用程序中提取有用的数据。
蒙特·蒙

55

最终路由器的用法

随着新路由器的推出,保护路由变得更加容易。您必须定义充当服务的防护,并将其添加到路由。

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

现在将传递LoggedInGuard给路由,并将其添加到providers模块数组中。

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

模块声明:

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

有关如何与最终版本一起使用的详细博客文章:https : //medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

不推荐使用的路由器的用法

一个更强大的解决方案是扩展,RouterOutlet并在激活路由检查时(如果用户已登录)在用户登录时使用。这种方式,您不必将指令复制并粘贴到每个组件。再加上基于子组件的重定向可能会产生误导。

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

UserService这里你的业务逻辑所在的用户是否登录或没有代表的地方。您可以在构造函数中使用DI轻松添加它。

当用户导航到您网站上的新URL时,将使用当前指令来调用activate方法。您可以从中获取URL并确定是否允许该URL。如果不是,则仅重定向到登录页面。

使它工作的最后一件事是将其传递给我们的主要组件,而不是内置组件。

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

该解决方案不能与@CanActive生命周期装饰器一起使用,因为如果传递给它的函数解析为false,RouterOutlet则不会调用的activate方法。

还写了一篇关于它的详细博客文章:https : //medium.com/@blacksonic86/authentication-in-angular-2-958052c64492


2
还写了关于它的更详细的博客文章medium.com/@blacksonic86/…–
Blacksonic

您好@Blacksonic。刚开始研究ng2。我遵循了您的建议,但最终在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". 无法计算出构造函数(_parentRounter)中要更改的内容以摆脱此消息。有什么想法吗?
leovrf

该声明是从底层内置对象RouterOutlet复制的,具有与扩展类相同的签名,我将为此行禁用特定的tslint规则
Blacksonic

我在mgechev 样式指南上找到了一个参考(查找“在@Attribute参数装饰器上首选输入”)。将行更改为_parentRouter: Router, @Input() nameAttr: string,和tslint不再引发错误。还替换了从角铁芯将“属性”导入到“输入”。希望这可以帮助。
leovrf

1
2.0.0-rc.1存在问题,因为没有导出RouterOutlet,也没有扩展它的可能性
mkuligowski 2016年

53

请不要覆盖路由器出口!最新的路由器版本(3.0 beta)是一场噩梦。

而是使用接口CanActivate和CanDeactivate并将路由定义中的类设置为canActivate / canDeactivate。

像那样:

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

类:

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

另请参阅:https : //angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard


2
不错,@ Blacksonic的答案对于不推荐使用的路由器非常适合我。升级到新路由器后,我不得不进行很多重构。您的解决方案正是我所需要的!
evandongen

我无法使canActivate在我的app.component中工作。如果没有通过身份验证,我想重定向用户。这是我拥有的路由器的版本(如果我需要更新它,如何使用git bash命令行执行该操作?)我拥有的版本:“ @ angular / router”:“ 2.0.0-rc.1”
AngularM

我可以使用同一类(AuthGuard)保护其他组件路由吗?
tsiro

4

遵循以上令人敬畏的答案,我也想CanActivateChild:保护儿童路线。它可用于向guard子路径添加对ACL等情况有用的路由

像这样

src / app / auth-guard.service.ts(摘录)

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(摘录)

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

这取自 https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard


2

请参考此代码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;      
    }
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.