Angular 重定向到登录页面。

146

我来自Asp.Net MVC领域,在那里,尝试访问未授权页面的用户会自动重定向到登录页面。

我正在尝试在Angular上重新产生这种行为。我发现了@CanActivate装饰器,但它导致组件根本不渲染,没有重定向。

我的问题是:

  • Angular是否提供实现此行为的方法?
  • 如果提供,如何实现?这是一个好实践吗?
  • 如果不提供,处理用户授权的最佳实践是什么?

我添加了一个实际的指令,展示如何进行身份验证,如果您有兴趣可以查看。 - Michael Oryl
此答案可能有用:https://dev59.com/vlkS5IYBdhLWcg3wVlTC#59008239 - AmirReza-Farahlagha
7个回答

158

这是一个使用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;
    }
}

您可以查看此文章,以获取完整示例和可工作演示。


7
我有一个后续问题,如果在localStorage中将currentUser设置为任意值,是否仍然可以访问受保护的路由?例如:localStorage.setItem('currentUser', 'dddddd') - jsdecena
2
它将绕过客户端安全性。但它也会清除令牌,这对于服务器端交易是必要的,因此无法从应用程序中提取有用的数据。 - Matt Meng
即使我已经登录,如果我输入类似于http://localhost:4200/#/login的网址,我仍然可以进入登录页面。如果用户已经登录,我们能否导航到其他页面而不是登录页面?请告知。 - Code_S
这应该是最好的答案。 - Shaze

93

更新: 我在Github上发布了一个完整的骨架Angular 2项目,其中包含OAuth2集成,展示了下面提到的指令。

一种实现方法是通过使用一个指令。与Angular 2 组件不同,组件基本上是您插入页面的新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>

那么你会想要在你的应用程序中导航/重定向用户到一个登录视图,并在那里处理身份验证。您需要将当前路由更改为所需的路由。因此,在这种情况下,您将使用依赖注入来获取指令的constructor()函数中的Router对象,然后使用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
好的,翻译如下: 它可行了!谢谢! 我在这里也发现另一种方法 - https://github.com/auth0/angular2-authentication-sample/blob/master/src/app/LoggedInOutlet.ts 我不能说哪种方法更好,但也许有人会觉得它也很有用。 - Sergey
3
谢谢!我还添加了一个新的路由,其中包含一个参数/protected/:returnUrl,returnUrl是在指令的ngOnInit中拦截的location.path()。这样可以在登录后导航用户到最初提示的URL。 - Amaury
2
请见下面的答案,这个常见问题(如果未经身份验证则重定向)应该有一个简单的解决方案,只需要一个句子就能回答。 - Rick O'Shea
8
注意:本答案涉及 Angular 2 的 beta 或 release-candidate 版本,不适用于 Angular 2 final 版本。请留意。 - jbandi
2
现在使用Angular Guards有一个更好的解决方案。 - mwilson
更好的解决方案是Jason的https://dev59.com/XVsX5IYBdhLWcg3wB7n3#38968963。 - Sisir

58

在最终路由器中的使用

随着新路由器的引入,保护路由变得更加容易。您必须定义一个作为服务的守卫,并将其添加到路由中。

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是指存放业务逻辑的地方,不论用户是否已登录。您可以通过构造函数轻松添加它。

当用户在您的网站上导航到一个新的URL时,将使用当前Instruction调用activate方法。您可以从中获取URL并决定是否允许访问。如果不允许,则重定向到登录页面。

最后一件需要做的事情是将其传递给我们的主要组件,而不是内置组件。

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

由于如果传递给@CanActive生命周期装饰器的函数解析为false,RouterOutlet的激活方法将不会被调用,因此无法使用此解决方案。

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


2
还写了一篇更详细的博客文章 https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492 - VuesomeDev
1
2.0.0-rc.1存在问题,因为RouterOutlet未被导出,也没有扩展的可能性。 - mkuligowski
你是在使用router还是router-deprecated? - VuesomeDev
在新的路由器示例中,如果您有多个未受保护的路由会发生什么?例如,您从未声明它应该去任何地方的登录,因此我假设它将转到第一个不安全的路由,但如果我有多个不安全的路由并且想要转到第二个呢? - Jackie
以下是一个更完整的解决方案:http://jasonwatmore.com/post/2016/12/08/angular-2-redirect-to-previous-url-after-login-with-auth-guard - nima
显示剩余8条评论

56

请不要覆盖路由插座!这在最新的路由器版本(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
我无法在我的app.component中使canActivate起作用。我想要在用户未经身份验证时重定向用户。这是我所拥有的路由器版本(如果需要更新,我该如何使用git bash命令行进行更新?)我所拥有的版本为:“@angular/router”:“2.0.0-rc.1”。 - AngularM
我能否使用同一个类(AuthGuard)来保护另一个组件的路由? - tsiro

5
以下是上面精彩回答的补充:我也想使用CanActivateChild:守卫子路由。它可以用于为子路由添加守卫,有助于像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);
  }

/* . . . */
}
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中获取的。

3
请参考以下代码,文件为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]    },

]; 

2

1. 创建如下所示的守卫。 2. 安装ngx-cookie-service以获取外部SSO返回的cookie。 3. 在environment.ts中创建ssoPath(SSO登录重定向)。 4. 获取state.url并使用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;      
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接