เปลี่ยนเส้นทางเชิงมุมไปยังหน้าเข้าสู่ระบบ


122

ฉันมาจากโลก Asp.Net MVC ซึ่งผู้ใช้ที่พยายามเข้าถึงเพจที่พวกเขาไม่ได้รับอนุญาตจะถูกเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบโดยอัตโนมัติ

ฉันพยายามสร้างพฤติกรรมนี้ซ้ำใน Angular ฉันเจอมัณฑนากร @CanActivate แต่ผลลัพธ์ก็คือส่วนประกอบไม่แสดงผลเลยไม่มีการเปลี่ยนเส้นทาง

คำถามของฉันมีดังต่อไปนี้:

  • Angular มีวิธีในการบรรลุพฤติกรรมนี้หรือไม่?
  • ถ้าเป็นเช่นนั้นอย่างไร? เป็นแนวทางปฏิบัติที่ดีหรือไม่?
  • ถ้าไม่แนวทางปฏิบัติที่ดีที่สุดในการจัดการการอนุญาตผู้ใช้ใน Angular คืออะไร

ฉันได้เพิ่มคำสั่งจริงที่แสดงวิธีการตรวจสอบสิทธิ์หากคุณสนใจที่จะดู
Michael Oryl

คำตอบนี้อาจมีประโยชน์: stackoverflow.com/a/59008239/7059557
AmirReza-Farahlagha

คำตอบ:


86

อัปเดต:ฉันได้เผยแพร่โครงการโครงกระดูกAngular 2เต็มรูปแบบพร้อมการรวม OAuth2บน Github ซึ่งแสดงคำสั่งที่กล่าวถึงด้านล่างในการดำเนินการ

วิธีหนึ่งที่จะทำได้คือการใช้ไฟล์directive. ซึ่งแตกต่างจาก 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();
        }
    }
}

นี่เป็นการใช้บริการการตรวจสอบสิทธิ์ที่ฉันเขียนขึ้นเพื่อตรวจสอบว่าผู้ใช้เข้าสู่ระบบแล้วหรือไม่และยังสมัครรับเหตุการณ์การตรวจสอบสิทธิ์เพื่อให้สามารถเตะผู้ใช้ออกหากเขาออกจากระบบหรือหมดเวลา

คุณสามารถทำสิ่งเดียวกัน คุณต้องสร้างคำสั่งเหมือนของฉันที่ตรวจสอบว่ามีคุกกี้ที่จำเป็นหรือข้อมูลสถานะอื่น ๆ ที่ระบุว่าผู้ใช้นั้นได้รับการพิสูจน์ตัวตน หากพวกเขาไม่มีแฟล็กที่คุณกำลังมองหาให้เปลี่ยนเส้นทางผู้ใช้ไปยังเพจสาธารณะหลักของคุณ (เช่นฉัน) หรือเซิร์ฟเวอร์ OAuth2 ของคุณ (หรืออะไรก็ตาม) คุณจะใส่แอตทริบิวต์คำสั่งนั้นไว้ในส่วนประกอบใด ๆ ที่จำเป็นต้องได้รับการปกป้อง ในกรณีนี้อาจเรียกว่าprotectedเหมือนในคำสั่งที่ฉันวางไว้ด้านบน

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

จากนั้นคุณจะต้องการนำทาง / เปลี่ยนเส้นทางผู้ใช้ไปยังมุมมองการเข้าสู่ระบบภายในแอปของคุณและจัดการการตรวจสอบสิทธิ์ที่นั่น คุณจะต้องเปลี่ยนเส้นทางปัจจุบันเป็นเส้นทางที่คุณต้องการทำ ดังนั้นในกรณีนี้คุณจะต้องใช้การฉีดพึ่งพาเพื่อรับวัตถุเราเตอร์ใน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/…ฉันไม่สามารถบอกได้ว่าวิธีใดดีกว่า แต่อาจมีคนพบว่ามีประโยชน์เช่นกัน
Sergey

3
ขอบคุณ ! ฉันยังเพิ่มเส้นทางใหม่ที่มีพารามิเตอร์ / ป้องกัน /: returnUrl, returnUrl เป็น location.path () ถูกสกัดกั้นที่ ngOnInit ของคำสั่ง สิ่งนี้ช่วยให้สามารถนำทางผู้ใช้หลังจากเข้าสู่ระบบไปยัง URL ที่แจ้งในตอนแรก
Amaury

1
ดูคำตอบด้านล่างสำหรับวิธีง่ายๆ สิ่งที่พบบ่อยนี้ (เปลี่ยนเส้นทางหากไม่ได้รับการพิสูจน์ตัวตน) ควรมีวิธีแก้ปัญหาง่ายๆด้วยคำตอบประโยคเดียว
Rick O'Shea

7
หมายเหตุ: คำตอบนี้กล่าวถึง Angular 2 รุ่นเบต้าหรือรุ่นที่เปิดตัวและใช้ไม่ได้กับ 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
ฉันมีคำถามติดตามหากตั้งค่าตามอำเภอใจเป็นcurrentUserในlocalStorageจะยังสามารถเข้าถึงเส้นทางที่ได้รับการป้องกันได้หรือไม่? เช่น. localStorage.setItem('currentUser', 'dddddd')?
jsd

2
มันจะข้ามการรักษาความปลอดภัยฝั่งไคลเอ็นต์ แต่ก็จะล้างโทเค็นที่จำเป็นสำหรับการทำธุรกรรมฝั่งเซิร์ฟเวอร์ดังนั้นจึงไม่สามารถดึงข้อมูลที่เป็นประโยชน์ออกจากแอปพลิเคชันได้
Matt Meng

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 ใหม่บนเว็บไซต์ของคุณวิธีการเปิดใช้งานจะถูกเรียกใช้ด้วยคำสั่งปัจจุบัน จากนั้นคุณสามารถคว้า url และตัดสินใจว่าอนุญาตหรือไม่ หากไม่ใช่แค่เปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบ

สิ่งสุดท้ายที่ยังคงอยู่เพื่อให้มันใช้งานได้คือการส่งต่อไปยังส่วนประกอบหลักของเราแทนที่จะสร้างขึ้นในชิ้นเดียว

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

ไม่สามารถใช้โซลูชันนี้กับ@CanActiveมัณฑนากรอายุการใช้งานได้เนื่องจากหากฟังก์ชันที่ส่งผ่านไปแก้ไขค่าเท็จRouterOutletจะไม่เรียกวิธีการเปิดใช้งานของฟังก์ชัน

เขียนบล็อกโพสต์โดยละเอียดเกี่ยวกับเรื่องนี้: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492


2
ยังเขียนบล็อกโพสต์โดยละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้medium.com/@blacksonic86/…
Blacksonic

สวัสดี @Blacksonic เพิ่งเริ่มขุดเป็น ng2 ฉันทำตามคำแนะนำของคุณ แต่สุดท้ายได้รับข้อผิดพลาดนี้ในช่วงอึก - ซลินท์: 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 style-guide (ค้นหา "ต้องการอินพุตมากกว่าตัวตกแต่งพารามิเตอร์ @Attribute") เปลี่ยนบรรทัดเป็น_parentRouter: Router, @Input() nameAttr: string,และ tslint ไม่ทำให้เกิดข้อผิดพลาดอีกต่อไป แทนที่การนำเข้า "แอตทริบิวต์" เป็น "อินพุต" จากแกนเชิงมุม หวังว่านี่จะช่วยได้
leovrf

1
มีปัญหากับ 2.0.0-rc.1 เนื่องจาก RouterOutlet ไม่ได้ถูกส่งออกและไม่มีความเป็นไปได้ที่จะขยายออก
mkuligowski

53

กรุณาอย่าลบล้าง Router Outlet! เป็นฝันร้ายกับเราเตอร์รุ่นล่าสุด (3.0 เบต้า)

ใช้อินเทอร์เฟซ 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 ใช้ได้ผลกับเราเตอร์ที่เลิกใช้แล้ว ฉันต้อง refactor มากหลังจากอัปเกรดเป็นเราเตอร์ใหม่ ทางออกของคุณคือสิ่งที่ฉันต้องการ!
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;      
    }
  }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.