首页 > 解决方案 > Angular - Auth Guard 防止授权用户通过直接 URL 访问身份验证页面

问题描述

 src
  |_auth/
    |_authentication/
    |_auth-service.ts
    |_auth-guard.ts
    |_is-logged-guard.ts
  |_dashboard/

auth-guard-service.ts

export class AuthService {
  public user: Observable<firebase.User>;
  public userDetails: firebase.User = null;
  public userProfileRef: firebase.database.Reference;
  userData: any[] = [];
  constructor(private _firebaseAuth: AngularFireAuth, private router: Router) {
    this.user = _firebaseAuth.authState;
    this.userProfileRef = firebase.database().ref('/userProfile');
        this.user.subscribe(
      (user) => {
        if (user) {
          this.userDetails = user;
        } else {
          this.userDetails = null;
        }
      }
    );
  }

  isLoggedIn() {
    if (this.userDetails == null) {
      return false;
    } else {
      return true;
    }
  }

  doSignOut() {
    this._firebaseAuth.auth.signOut()
      .then((res) => this.router.navigate(['/auth/login']));
  }
}


auth-guard.ts

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private auth: AuthService, private router: Router) { }

  canActivate() {
    return this.auth.user.take(1).map(authState => !!authState).do(authenticated => { new Promise<boolean>( (resolve, reject) => {
      if (!authenticated) {
        this.router.navigate(['auth/sigin']);
        return resolve(false);
      } else {
        return resolve(true);
      }
  }

}

is-logged-guard.ts - 我知道这是问题所在。我将如何解决它?

@Injectable()
export class IsLoggedGuard implements CanActivate {

  constructor(private auth: AuthService, private router: Router) { }

  canActivate() {
    return !this.auth.isLoggedIn();
  }

}

应用程序路由.module.ts


const routes: Routes = [
  { path: 'dashboard',
    canActivate: [AuthGuard],
    loadChildren: './dashboard/dashboard.module#DashboardModule'
  },
  {
    path: 'auth',
    component: NbAuthComponent,
    canActivate: [IsLoggedGuard],
    children: [
      {
        path: '',
        component: SignInComponent,
      },
      {
        path: 'SignIn',
        component: SignInComponent,
      },
      {
        path: 'SignUp',
        component: SignUpComponent,
      },
    ],
  },
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  { path: '**', redirectTo: 'dashboard' },
];

const config: ExtraOptions = {
  useHash: true,
};

@NgModule({
  imports: [RouterModule.forRoot(routes, config)],
  exports: [RouterModule],
})
export class AppRoutingModule {
}

案例一:用户未登录

没问题。身份验证保护保护仪表板免受未经身份验证的用户的攻击,并将他们重定向到身份验证页面(即登录页面)。

案例 2:用户已经登录 #

没问题。如果经过身份验证的用户通过 localhost:4200 或 localhost:4200/#/dashboard 或 localhost:4200/#/ 或 localhost:4200/#/RANDOM_INVALID_URL 访问仪表板,则一切正常。守卫还将阻止已经在仪表板内的经过身份验证的用户访问身份验证页面。

案例3:用户已经登录

问题。如果经过身份验证的用户通过 localhost:4200/#/auth 或 localhost:4200/#/auth/signin 访问仪表板,则守卫将无法保护并将用户重定向到仪表板主页。(即 John 已经登录并打开一个新的 Chrome 选项卡,并输入 localhost:4200/#/auth 守卫不会阻止他访问它)。如果 John 已经通过身份验证,我该如何修复我的守卫以防止他访问身份验证页面?

标签: angulartypescriptfirebase

解决方案


您应该像这样更改您的 IsLoggedGuard:

@Injectable()
export class IsLoggedGuard implements CanActivate {

  constructor(private auth: AuthService, private router: Router) { }

  canActivate() {
    return this.auth.user
                    .take(1)
                    .map(authState => {
                       if (authState) {
                          //user is already loggedin
                          //route the user to Dashboard page
                          //Or a page where you want the app to naviagte
                          this.router.navigate("dashboard route");
                          //dont show the Login page
                          return false;
                       } else {
                         //user is not loggedin
                         return true;
                       }
                    });
  }

}

您看到了这个问题,因为当您在浏览器中输入“localhost:4200/#/auth”url 时,您的AuthGuard.user.subscribe[ie in the constructor this.user.subscribe(] 在执行时可能尚未发出任何值IsLoggedGuard's canActivate()[ie AuthService.isLoggedIn() 可能会返回false因为订阅回调可能尚未执行(填充用户详细信息)]。

让我知道它是否能解决您的问题。

可能有更好的方法来实现您的 AuthService 以及使用 AuthService 的 Guards。如果您想要更好的代码,请告诉我。

编辑 - 另一种编写 AuthService 的方法

让我们像这样更改 AuthService:

export class AuthService {

    //NOTE: I AM JUST SHOWING TWO THINGS - isUserLoggedIn AND userDetails
    //FROM THIS CODE YOU WILL GET AN IDEA HOW TO WRITE OTHER PROPERTIES WHICH ARE RELEVANT FOR YOUR APP

    //This will be used as a source for various observables
    private _authState$: Observable<any>;

    //Have an observable which will tell if user is loggedin or not
    isUserLoggedIn$: Observable<boolean>;
    userDetails$: Observable<firebase.User>;

    public userProfileRef: firebase.database.Reference;

    constructor(private _firebaseAuth: AngularFireAuth, private router: Router) {            
      this.userProfileRef = firebase.database().ref('/userProfile');
      this.setupObserables();
    }

    setupObserables() {

        // this observable will broadcast the emited values to multiple subscribers [or composed/dependent observables]
        this._authState$ = this._firebaseAuth.authState
                                        .publishReplay(1)
                                        .refCount();

        // lets componse/derive different observables required by the consumer of this service

        // This observable's emitted value will tell if user is logged in or not
        this.isUserLoggedIn$ = this._authState$
                                   .map(user => {
                                        return user ? true : false;
                                    });

        // This observable's emited value will return the user's detail [NOTE If user is not logged in then the emitted value will be NULL
        // i.e. userDetail is NULL; Your consumer of this observable should decide what to do with NULL/NOT NULL Value]        
        this.userDetails$ = this._authState$
                                .map(user => user);
    }    

    doSignOut() {
      this._firebaseAuth.auth.signOut()
        .then((res) => this.router.navigate(['/auth/login']));
    }
  }

现在让我们在 IsLoggedGuard 中使用更新的 AuthService:

    @Injectable()
    export class IsLoggedGuard implements CanActivate {

      constructor(private auth: AuthService, private router: Router) { }

      canActivate() {
        return this.auth.isUserLoggedIn$
                        .take(1)
                        .map(isLoggedIn => {
                           if (isLoggedIn) {
                              //user is already loggedin
                              //route the user to Dashboard page
                              //Or a page where you want the app to naviagte
                              this.router.navigate("dashboard route");
                              //dont show the Login page
                              return false;
                           } else {
                             //user is not loggedin
                             return true;
                           }
                        });
      }

    }

现在让我们在 AuthGuard 中使用更新的 AuthService:

    @Injectable()
    export class AuthGuard implements CanActivate {

    constructor(private auth: AuthService, private router: Router) { }

    canActivate() {
        return this.auth.isUserLoggedIn$
                   .take(1)
                   .map(isLoggedIn => {
                    if (!isLoggedIn) {
                       //user isNOT loggedin
                       //route the user to login page
                       this.router.navigate(['auth/sigin']);
                       //dont show the next route
                       //lets fail the guard
                       return false;
                    } else {
                      //user is loggedin; pass the guard i.e. show the next route associated with this guard
                      return true;
                    }
                 });
        }

    }

现在假设某个组件​​(假设组件名称是UserComponent)要显示登录用户的详细信息:

....component decorator...
export class UserComponent implements OnInit {

    userDetails$: Observable<User>;
    constructor(private _authService: AuthService) {
        this.userDetails$ = this._authService.userDetails$;
    }
}

像这样渲染 userDetails:

<div *ngIf="(userDetails$ | async) as userDetails">
    <!-- Render your user details here -->
    <!-- If userDetails is NULL then nothing will be rendered -->
</div>

注意事项 -在此更新后的代码中,我们在 NOWHERE 中订阅任何可观察对象。请注意async,在组件模板中,它负责订阅/取消订阅已使用的 observable。

希望它会给你一个方向/想法。让我们尽可能地“反应”而不是“命令式”..:)

注意:我已经测试了 rxjs6 中的等效代码。看起来您正在使用 rxjs5,所以我已经根据 rxjs5 调整了发布的代码。希望它会奏效。


推荐阅读