angular私有模块
在本文中,我们将向Angular应用程序添加身份验证,并了解如何保护应用程序中的节免受未经授权的访问。
本文是SitePoint Angular 2+教程的第5部分,该教程介绍如何使用Angular CLI创建CRUD应用程序。
- 第0部分— Ultimate Angular CLI参考指南
- 第1部分-启动并运行我们的Todo应用程序的第一个版本
- 第2部分-创建单独的组件以显示待办事项列表和一个待办事项
- 第3部分-更新Todo服务以与REST API通信
- 第4部分-使用Angular路由器解析数据
- 第5部分-添加身份验证以保护私有内容
- 第6部分—如何将Angular项目更新到最新版本。
在第1部分中,我们学习了如何启动和运行Todo应用程序并将其部署到GitHub页面。 这样做很好,但不幸的是,整个应用程序都挤在一个组件中。
在第2部分中,我们研究了模块化程度更高的组件体系结构,并学习了如何将单个组件分解为较小的组件的结构化树,这些树更易于理解,重用和维护。
在第3部分中,我们更新了应用程序以使用RxJS和Angular的HTTP服务与REST API后端进行通信。
在第4部分中 ,我们介绍了Angular Router,并了解了当浏览器URL更改时路由器如何更新我们的应用程序,以及如何使用路由器从后端API解析数据。
不用担心 您无需遵循本教程的第1、2、3或4部分,就可以使第5部分有意义。 您可以简单地获取我们的仓库的副本,查看第4部分中的代码,并将其用作起点。 下面将对此进行详细说明。
启动并运行
确保您已安装最新版本的Angular CLI。 如果没有安装,则可以使用以下命令进行安装:
npm install -g @angular/cli@latest
如果需要删除以前版本的Angular CLI,可以运行以下命令:
npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/cli@latest
之后,您将需要第4部分中的代码的副本。可从https://github.com/sitepoint-editors/angular-todo-app获得 。 本系列中的每篇文章在存储库中都有一个相应的标记,因此您可以在应用程序的不同状态之间来回切换。
我们在第4部分中结束并在本文中开始的代码被标记为第4部分 。 本文结尾处的代码被标记为part-5 。
您可以将标签视为特定提交ID的别名。 您可以使用git checkout
在它们之间切换。 您可以在此处阅读更多内容 。
因此,要启动并运行(安装了最新版本的Angular CLI),我们可以这样做:
git clone git@github.com:sitepoint-editors/angular-todo-app.git
cd angular-todo-app
git checkout part-4
npm install
ng serve
然后访问http:// localhost:4200 / 。 如果一切顺利,您应该会看到正在运行的Todo应用程序。
进攻计划
在本文中,我们将:
- 设置一个后端进行身份验证
- 向我们现有的
ApiService
添加登录方法 - 设置身份验证服务以处理身份验证逻辑
- 设置会话服务以存储会话数据
- 创建一个
SignInComponent
来显示登录表单 - 设置路由防护,以保护我们的应用程序的某些部分免受未经授权的访问。
到本文结尾,您将了解:
- Cookie和令牌之间的区别
- 如何创建
AuthService
来实现身份验证逻辑 - 如何创建一个
SessionService
来存储会话数据 - 如何使用AngularReact形式创建登录形式
- 如何创建路由防护以防止未经授权访问应用程序的某些部分
- 如何在HTTP请求中将用户令牌作为授权标头发送到您的API
- 为什么您永远不应该将用户的令牌发送给第三方。
我们的应用程序将如下所示:
所以,让我们开始吧!
认证策略
![](https://i-blog.csdnimg.cn/blog_migrate/6d7e39b322599da95c5c42c4fc503ab9.png)
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
服务器端Web应用程序通常会处理服务器上的用户会话。 它们将会话详细信息存储在服务器上,并通过cookie将会话ID发送到浏览器。 浏览器存储cookie,并在每次请求时自动将其发送到服务器。 然后,服务器从cookie中获取会话ID,并从其内部存储(内存,数据库等)中查找相应的会话详细信息。 会话详细信息保留在服务器上,而在客户端中不可用。
相反,客户端Web应用程序(例如Angular应用程序)通常在客户端中管理用户会话。 会话数据存储在客户端中,并在需要时发送到服务器。 在客户端中存储会话的标准方法是JSON Web令牌 ,也称为JWT令牌。 如果您不熟悉令牌的工作方式,请查看此简单的隐喻以轻松理解并记住基于令牌的身份验证的工作方式,并且您将永远不会忘记。
如果您想对Cookie和令牌有更深入的了解,请务必查看Philippe De Ryck的Cookie与令牌的对话:一个悖论的选择 。
由于JSON Web令牌在当今的生态系统中非常流行,因此我们将使用基于JWT的身份验证策略。
设置后端
在向Angular应用程序添加身份验证之前,我们需要一个后端进行身份验证。
在本系列的前几部分中,我们使用json-server基于项目根目录中的db.json
文件提供后端数据。
幸运的是,json-server 也可以作为节点模块加载 ,从而允许我们添加自定义请求处理程序。
让我们首先安装body-parser npm模块,该模块需要解析HTTP请求中的JSON:
$ npm install --save body-parser
接下来,我们在项目的根目录中创建一个新文件json-server.js
:
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('db.json');
const middlewares = jsonServer.defaults();
const bodyParser = require('body-parser');
// Sample JWT token for demo purposes
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' +
'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg';
// Use default middlewares (CORS, static, etc)
server.use(middlewares);
// Make sure JSON bodies are parsed correctly
server.use(bodyParser.json());
// Handle sign-in requests
server.post('/sign-in', (req, res) => {
const username = req.body.username;
const password = req.body.password;
if(username === 'demo' && password === 'demo') {
res.json({
name: 'SitePoint Reader',
token: jwtToken
});
}
res.send(422, 'Invalid username and password');
});
// Protect other routes
server.use((req, res, next) => {
if (isAuthorized(req)) {
console.log('Access granted');
next();
} else {
console.log('Access denied, invalid JWT');
res.sendStatus(401);
}
});
// API routes
server.use(router);
// Start server
server.listen(3000, () => {
console.log('JSON Server is running');
});
// Check whether request is allowed
function isAuthorized(req) {
let bearer = req.get('Authorization');
if (bearer === 'Bearer ' + jwtToken) {
return true;
}
return false;
}
本文并不是要作为json-server上的教程,但让我们快速看一下发生了什么。
首先,我们导入所有json-server机制:
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('db.json');
const middlewares = jsonServer.defaults();
const bodyParser = require('body-parser');
在实际的应用程序中,当用户进行身份验证时,我们将动态生成JWT令牌,但是出于演示的目的,我们静态定义了JWT令牌:
// Sample JWT token for demo purposes
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' +
'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg';
接下来,我们将json-server配置为运行其自己的默认中间件:
// Use default middlewares (CORS, static, etc)
server.use(middlewares);
并正确解析传入的JSON请求:
// Make sure JSON bodies are parsed correctly
server.use(bodyParser.json());
Json-server的默认中间件是处理静态文件,CORS等的请求处理程序功能。有关更多详细信息,请查阅文档 。
然后,我们为登录请求定义一个请求处理程序:
// Handle sign-in requests
server.post('/sign-in', (req, res) => {
const username = req.body.username;
const password = req.body.password;
if(username === 'demo' && password === 'demo') {
res.json({
name: 'SitePoint Reader',
token: jwtToken
});
}
res.send(422, 'Invalid username and password');
});
我们告诉json-server监听/sign-in
上的HTTP POST请求。 如果请求包含的值的用户名现场demo
和密码字段的值demo
,我们与JWT令牌返回一个对象。 如果不是,我们将发送HTTP 422响应以指示用户名和密码无效。
另外,我们还告诉json-server授权所有其他请求:
// Protect other routes
server.use((req, res, next) => {
if (isAuthorized(req)) {
console.log('Access granted');
next();
} else {
console.log('Access denied, invalid JWT');
res.sendStatus(401);
}
});
// Check whether request is allowed
function isAuthorized(req) {
let bearer = req.get('Authorization');
if (bearer === 'Bearer ' + jwtToken) {
return true;
}
return false;
}
如果客户端的HTTP请求包含带有JWT令牌的Authorization标头,则我们授予访问权限。 如果没有,我们将拒绝访问并发送HTTP 401响应。
最后,我们告诉json-server从db.json
加载API路由并启动服务器:
// API routes
server.use(router);
// Start server
server.listen(3000, () => {
console.log('JSON Server is running');
});
要开始我们的新后端,我们运行:
$ node json-server.js
为了方便起见,让我们更新package.json
的json-server
脚本:
"json-server": "node json-server.js"
现在我们可以运行:
$ npm run json-server
> todo-app@0.0.0 json-server /Users/jvandemo/Projects/sitepoint-editors/angular-todo-app
> node json-server.js
JSON Server is running
瞧,我们有自己的具有身份验证功能的API服务器。
是时候挖掘Angular了。
向我们的API服务添加身份验证逻辑
现在我们有了一个要进行身份验证的API端点,让我们向ApiService
添加一个新方法来执行身份验证请求:
@Injectable()
export class ApiService {
constructor(
private http: Http
) {
}
public signIn(username: string, password: string) {
return this.http
.post(API_URL + '/sign-in', {
username,
password
})
.map(response => response.json())
.catch(this.handleError);
}
// ...
}
调用时, signIn()
方法将向我们新的/sign-in
API端点执行HTTP POST请求,包括请求正文中的用户名和密码。
如果您不熟悉Angular的内置HTTP服务,请确保阅读第3部分-更新Todo服务以与REST API通信 。
创建会话服务
现在我们有了一个可以对后端进行身份验证的API方法,我们需要一种机制来存储从API接收到的会话数据,即name
和token
。
由于数据在整个应用程序中将是唯一的,因此我们将其存储在一个名为SessionService
的服务中。
因此,让我们生成新的SessionService:
$ ng generate service session --module app.module.ts
create src/app/session.service.spec.ts
create src/app/session.service.ts
update src/app/app.module.ts
--module app.module.ts
部分告诉Angular CLI自动将我们的新服务注册为AppModule
的提供者,这样我们就不必手动注册它。 需要将服务注册为提供者,以便Angular依赖项注入器可以在需要时实例化它。 如果您不熟悉Angular依赖项注入系统,请确保查看官方文档 。
打开src/app/session.service.ts
并添加以下代码:
import { Injectable } from '@angular/core';
@Injectable()
export class SessionService {
public accessToken: string;
public name: string;
constructor() {
}
public destroy(): void {
this.accessToken = null;
this.name = null;
}
}
我们让事情变得非常简单。 我们定义一个属性来存储用户的API访问令牌,并定义一个属性来存储用户的名称。
我们还添加了方法destroy()
来重置所有数据,以防我们希望退出当前用户。
注意SessionService
如何不知道任何身份验证逻辑。 它仅负责存储会话数据。
我们将创建一个单独的AuthService
来实现实际的身份验证逻辑。
创建身份验证服务
将身份验证逻辑放在单独的服务中可以促进身份验证过程与会话数据存储之间的良好关注分离。
这确保了如果身份验证流程发生更改,我们就不必更改SessionService
,并允许我们轻松地在单元测试中模拟会话数据。
因此,让我们创建一个名为AuthService
的服务:
$ ng generate service auth --module app.module.ts
create src/app/auth.service.spec.ts
create src/app/auth.service.ts
update src/app/app.module.ts
打开src/app/auth.service.ts
并添加以下代码:
import { Injectable } from '@angular/core';
import { SessionService } from './session.service';
@Injectable()
export class AuthService {
constructor(
private session: SessionService,
) {
}
public isSignedIn() {
return !!this.session.accessToken;
}
public doSignOut() {
this.session.destroy();
}
public doSignIn(accessToken: string, name: string) {
if ((!accessToken) || (!name)) {
return;
}
this.session.accessToken = accessToken;
this.session.name = name;
}
}
我们注入SessionService
并添加一些方法:
-
isSignedIn()
:返回用户是否已登录 -
doSignOut()
:通过清除会话数据来注销用户 -
doSignIn()
:通过存储会话数据来登录用户。
再次注意,在SessionService
用于存储实际会话数据时,如何在AuthService
定义身份验证逻辑。
现在我们已经有了身份验证服务,让我们创建一个带有身份验证表单的登录页面。
创建登录页面
让我们使用Angular CLI创建一个SignInComponent
:
$ ng generate component sign-in
create src/app/sign-in/sign-in.component.css
create src/app/sign-in/sign-in.component.html
create src/app/sign-in/sign-in.component.spec.ts
create src/app/sign-in/sign-in.component.ts
update src/app/app.module.ts
我们的登录形式将是AngularReact形式 ,因此我们必须在src/app/app.module.ts
中的应用程序模块中导入ReactiveFormsModule
:
// ...
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
// ...
],
imports: [
// ...
ReactiveFormsModule
],
providers: [
// ...
],
bootstrap: [AppComponent]
})
export class AppModule {
}
接下来,我们将TypeScript代码添加到src/app/sign-in/sign-in.component.ts
:
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../api.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-sign-in',
templateUrl: './sign-in.component.html',
styleUrls: ['./sign-in.component.css']
})
export class SignInComponent implements OnInit {
public frm: FormGroup;
public isBusy = false;
public hasFailed = false;
public showInputErrors = false;
constructor(
private api: ApiService,
private auth: AuthService,
private fb: FormBuilder,
private router: Router
) {
this.frm = fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
ngOnInit() {
}
public doSignIn() {
// Make sure form values are valid
if (this.frm.invalid) {
this.showInputErrors = true;
return;
}
// Reset status
this.isBusy = true;
this.hasFailed = false;
// Grab values from form
const username = this.frm.get('username').value;
const password = this.frm.get('password').value;
// Submit request to API
this.api
.signIn(username, password)
.subscribe(
(response) => {
this.auth.doSignIn(
response.token,
response.name
);
this.router.navigate(['todos']);
},
(error) => {
this.isBusy = false;
this.hasFailed = true;
}
);
}
}
首先,我们在构造函数中实例化React形式:
this.frm = fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
我们将React式表单定义为包含两个表单控件的表单组-一个用于用户名,一个用于密码。 这两个控件的默认值为空字符串''
,并且两个控件都需要一个值。
如果您不熟悉React式,请确保查看Angular网站上的官方文档 。
接下来,我们定义一个doSignIn()
方法:
public doSignIn() {
// Make sure form values are valid
if (this.frm.invalid) {
this.showInputErrors = true;
return;
}
// Reset status
this.isBusy = true;
this.hasFailed = false;
// Grab values from form
const username = this.frm.get('username').value;
const password = this.frm.get('password').value;
// Submit request to API
this.api
.signIn(username, password)
.subscribe(
(response) => {
this.auth.doSignIn(
response.token,
response.name
);
this.router.navigate(['todos']);
},
(error) => {
this.isBusy = false;
this.hasFailed = true;
}
);
}
首先,我们检查表单是否处于有效状态。 在构造函数中,我们使用Angular的内置Validators.required
验证程序配置了username
和password
表单控件。 这将两个控件都标记为必需,并且一旦表单控件之一具有空值,就会导致表单处于无效状态。
如果表单处于无效状态,则启用showInputErrors
并返回而不调用API。
如果窗体处于有效状态(包括username
和password
有一个值),我们设置isBusy
为true,并调用了signIn()
我们的方法ApiService
。 在进行API调用时,我们将使用isBusy
变量在视图中禁用登录按钮。
如果API调用成功,我们称之为doSignIn()
的方法AuthService
与token
和name
来自API的响应和对用户进行导航到todos
路线。
如果API调用失败, isBusy
标记为false
,将hasFailed
标记为true
以便我们可以重新启用登录按钮并在视图中显示错误消息。
现在我们已经有了组件的控制器,让我们将其相应的视图模板添加到src/app/sign-in/sign-in.component.ts
:
<div class="sign-in-wrapper">
<form [formGroup]="frm">
<h1>Todos</h1>
<!-- Username input -->
<input type="text" formControlName="username" placeholder="Your username">
<!-- Username validation message -->
<div
class="input-errors"
*ngIf="(frm.get('username').invalid && frm.get('username').touched) || showInputErrors"
>
<div *ngIf="frm.get('username').hasError('required')">
Please enter your username
</div>
</div>
<!-- Password input -->
<input type="password" formControlName="password" placeholder="Your password">
<!-- Password validation message -->
<div
class="input-errors"
*ngIf="(frm.get('password').invalid && frm.get('password').touched) || showInputErrors"
>
<div *ngIf="frm.get('password').hasError('required')">
Please enter your password
</div>
</div>
<!-- Sign-in error message -->
<div class="sign-in-error" *ngIf="hasFailed">
Invalid username and password.
</div>
<!-- Sing-in button -->
<button (click)="doSignIn()" [disabled]="isBusy">
<ng-template [ngIf]="!isBusy">Sign in</ng-template>
<ng-template [ngIf]="isBusy">Signing in, please wait...</ng-template>
</button>
<!-- Tip -->
<p class="tip">You can sign in with username "demo" and password "demo".</p>
</form>
</div>
首先,我们定义一个表单元素,并使用[formGroup]="frm"
将其绑定到控制器中的React形式。
在表单内部,我们为用户名添加了一个input元素,并使用formControlName="username"
将其绑定到其相应的表单控件。
接下来,我们添加验证错误以显示用户名是否无效。 请注意,我们如何使用便捷的属性(由Angular提供),例如valid
, invalid
, pristine
, dirty
, untouched
touched
以缩小我们想要显示验证消息的条件。 在这里,我们要在用户名无效并且用户触摸输入时显示验证错误。 此外,我们还希望在用户单击“登录”按钮并且输入没有任何值时显示验证错误。
我们对密码输入重复相同的模式,并添加一条常规错误消息以在用户名和密码不是有效凭据的情况下显示。
最后,我们添加提交按钮:
<button (click)="doSignIn()" [disabled]="isBusy">
<ng-template [ngIf]="!isBusy">Sign in</ng-template>
<ng-template [ngIf]="isBusy">Signing in, please wait...</ng-template>
</button>
当用户单击按钮并进行API调用时,我们使用[disabled]="isBusy"
禁用按钮并更改其文本,以便用户直观地看到登录过程很忙。
现在我们已经有了登录页面,让我们在`src/app/app-routing.module.ts
重新配置路由:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SignInComponent } from './sign-in/sign-in.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { TodosComponent } from './todos/todos.component';
import { TodosResolver } from './todos.resolver';
const routes: Routes = [
{
path: '',
redirectTo: 'sign-in',
pathMatch: 'full'
},
{
path: 'sign-in',
component: SignInComponent
},
{
path: 'todos',
component: TodosComponent,
resolve: {
todos: TodosResolver
}
},
{
path: '**',
component: PageNotFoundComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
TodosResolver
]
})
export class AppRoutingModule { }
我们定义了一条新的sign-in
路线:
{
path: 'sign-in',
component: SignInComponent
}
并将默认网址重定向到我们的新登录路线:
{
path: '',
redirectTo: 'sign-in',
pathMatch: 'full'
}
这样,在加载我们的应用程序时,用户会自动重定向到登录页面。
如果您运行:
$ ng serve
并将浏览器导航到http://localhost:4200
,您应该看到:
到目前为止,我们已经介绍了很多内容:
- 设置我们的后端
- 向我们的ApiService添加了一种方法来登录
- 为我们的验证逻辑创建了AuthService
- 创建了一个SessionService来存储我们的会话数据
- 创建了一个SignInComponent来登录用户。
但是,如果我们使用用户名演示和密码演示登录,则当我们请求待办事项时,API会返回错误401:
另外,即使我们未登录,Angular仍然允许我们将浏览器直接导航到http://localhost:4200/todos
。
为了解决这两个问题,我们现在将:
- 保护我们应用程序的私有区域,防止未登录用户未经授权访问
- 发送带有需要身份验证的API请求的用户令牌。
让我们从保护应用程序的私有区域开始。
保护我们应用程序的私有区域免受未经授权的访问
在第4部分中 ,我们已经学习了如何使用Angular Router解析数据。 在本节中,我们将探究路线防护,这是Angular Router的一项功能,可让我们控制路线导航。
本质上,路由保护是一个函数,返回true
表示允许路由,返回false
表示不允许路由。 警卫还可以返回评估为真实或虚假值的Promise或Observable。 在这种情况下,路由器将等待直到Promise或Observable完成。
有4种类型的路由防护 :
-
CanLoad
:确定是否可以加载延迟加载的模块 -
CanActivate
:确定当用户导航到该路线时是否可以激活该路线 -
CanActivateChild
:确定当用户导航到其子级之一时是否可以激活路由 -
CanDeactivate
:确定是否可以禁用路由。
在我们的应用程序中,我们希望确保用户导航到todos
路线时已登录。 因此,可以使用CanActivate
保护器。
让我们在一个名为src/app/can-activate-todos.guard.ts
的新文件中创建防护src/app/can-activate-todos.guard.ts
:
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class CanActivateTodosGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {
}
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
if (!this.auth.isSignedIn()) {
this.router.navigate(['/sign-in']);
return false;
}
return true;
}
}
因为我们的防护是CanActivate
防护,所以它需要实现@angular/router
提供的CanActivate
接口。
CanActivate
接口要求我们的卫士实现canActivate()
方法:
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
if (!this.auth.isSignedIn()) {
this.router.navigate(['/sign-in']);
return false;
}
return true;
}
canActivate()
方法接收激活的路由快照和路由器状态快照作为参数,以防我们需要它们做出明智的决定来决定是否允许导航。
在我们的示例中,逻辑非常简单。 如果用户未登录,我们会指示Angular路由器将用户导航到登录页面并停止进一步的导航。
相反,如果用户已登录,则返回true
允许用户导航到请求的路线。
现在我们已经创建了路由防护,我们必须告诉Angular路由器实际使用它。
因此,让我们在src/app/app-routing.module.ts
添加我们的路由配置:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SignInComponent } from './sign-in/sign-in.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { TodosComponent } from './todos/todos.component';
import { CanActivateTodosGuard } from './can-activate-todos.guard';
import { TodosResolver } from './todos.resolver';
const routes: Routes = [
{
path: '',
redirectTo: 'sign-in',
pathMatch: 'full'
},
{
path: 'sign-in',
component: SignInComponent
},
{
path: 'todos',
component: TodosComponent,
canActivate: [
CanActivateTodosGuard
],
resolve: {
todos: TodosResolver
}
},
{
path: '**',
component: PageNotFoundComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
CanActivateTodosGuard,
TodosResolver
]
})
export class AppRoutingModule { }
通过将canActivate
属性添加到路由,我们告诉Angular路由器在todos
路由上使用我们的守护程序:
{
path: 'todos',
component: TodosComponent,
canActivate: [
CanActivateTodosGuard
],
resolve: {
todos: TodosResolver
}
}
canActivate
属性接受一个CanActivate
保护数组,因此,如果您的应用程序需要它,则可以轻松注册多个保护。
最后,我们需要添加CanActivateTodosGuard
作为提供程序,以便Angular的依赖项注入器可以在路由器要求时实例化它:
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
CanActivateTodosGuard,
TodosResolver
]
})
export class AppRoutingModule { }
有了我们的路线防护之后,我们的应用程序现在可以在用户未登录时将其重定向到登录页面,并尝试直接导航到todos
路线。
相反,当用户登录时,允许导航到todos
路线。
多么甜蜜!
使用API请求发送用户令牌
到目前为止,我们的登录用户可以访问todos
路由,但是API仍拒绝返回任何todo数据,因为我们没有将用户的令牌发送到API。
因此,让我们打开src/app/api.service.ts
并告诉Angular在需要时在HTTP请求的标头中发送用户的令牌:
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response } from '@angular/http';
import { environment } from 'environments/environment';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { SessionService } from 'app/session.service';
const API_URL = environment.apiUrl;
@Injectable()
export class ApiService {
constructor(
private http: Http,
private session: SessionService
) {
}
public signIn(username: string, password: string) {
return this.http
.post(API_URL + '/sign-in', {
username,
password
})
.map(response => response.json())
.catch(this.handleError);
}
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.map(response => {
const todos = response.json();
return todos.map((todo) => new Todo(todo));
})
.catch(this.handleError);
}
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public deleteTodoById(todoId: number): Observable<null> {
const options = this.getRequestOptions();
return this.http
.delete(API_URL + '/todos/' + todoId, options)
.map(response => null)
.catch(this.handleError);
}
private handleError(error: Response | any) {
console.error('ApiService::handleError', error);
return Observable.throw(error);
}
private getRequestOptions() {
const headers = new Headers({
'Authorization': 'Bearer ' + this.session.accessToken
});
return new RequestOptions({ headers });
}
}
首先,我们定义一种方便的方法来创建我们的请求选项:
private getRequestOptions() {
const headers = new Headers({
'Authorization': 'Bearer ' + this.session.accessToken
});
return new RequestOptions({ headers });
}
接下来,我们更新与需要身份验证的API端点通信的所有方法:
public getAllTodos(): Observable<Todo[]> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos', options)
.map(response => {
const todos = response.json();
return todos.map((todo) => new Todo(todo));
})
.catch(this.handleError);
}
public createTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.post(API_URL + '/todos', todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public getTodoById(todoId: number): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.get(API_URL + '/todos/' + todoId, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public updateTodo(todo: Todo): Observable<Todo> {
const options = this.getRequestOptions();
return this.http
.put(API_URL + '/todos/' + todo.id, todo, options)
.map(response => {
return new Todo(response.json());
})
.catch(this.handleError);
}
public deleteTodoById(todoId: number): Observable<null> {
const options = this.getRequestOptions();
return this.http
.delete(API_URL + '/todos/' + todoId, options)
.map(response => null)
.catch(this.handleError);
}
我们使用便捷帮助器实例化请求选项,并将这些选项作为第二个参数传递给我们的http调用。
警告:要非常小心!
始终确保仅将令牌发送到受信任的API。 不要只是在每个传出的HTTP请求中盲目发送令牌。
例如:如果您的应用程序与第三方API通信,并且您不小心将用户的令牌发送到该第三方API,则第三方可以使用该令牌登录以代表您的用户查询您的API。 因此,请务必小心,仅将令牌发送给受信任的各方,并且仅与需要令牌的请求一起发送。
要了解有关基于令牌的身份验证的安全性方面的更多信息,请确保查看Philippe De Ryck的Cookie与令牌:悖论选择 。
如果将浏览器导航到http://localhost:4200
,则现在应该可以使用用户名demo和密码demo登录。
在我们的TodosComponent中添加退出按钮
为了完整起见,我们还要在待办事项列表下添加一个退出按钮。
让我们打开src/app/todos/todos.component.ts
并添加一个doSignOut()
方法:
import { Component, OnInit } from '@angular/core';
import { TodoDataService } from '../todo-data.service';
import { Todo } from '../todo';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-todos',
templateUrl: './todos.component.html',
styleUrls: ['./todos.component.css']
})
export class TodosComponent implements OnInit {
todos: Todo[] = [];
constructor(
private todoDataService: TodoDataService,
private route: ActivatedRoute,
private auth: AuthService,
private router: Router
) {
}
// ...
doSignOut() {
this.auth.doSignOut();
this.router.navigate(['/sign-in']);
}
}
首先,我们导入AuthService
和Router
。
接下来,我们定义一个doSignOut()
方法,该方法用于注销用户并将用户导航回到登录页面。
现在我们已经有了逻辑,让我们在src/app/todos/todos.component.html
按钮添加到视图中:
<!-- Todos -->
<section class="todoapp">
<app-todo-list-header
(add)="onAddTodo($event)"
></app-todo-list-header>
<app-todo-list
[todos]="todos"
(toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"
></app-todo-list>
<app-todo-list-footer
[todos]="todos"
></app-todo-list-footer>
</section>
<!-- Sign out button -->
<button (click)="doSignOut()">Sign out</button>
如果刷新浏览器并再次登录,则应该看到:
单击注销按钮将触发组件控制器中的doSignOut()
方法,将您带回到登录页面。
另外,如果您注销并尝试直接将浏览器导航到http://localhost:4200/todos
,则路由防护将检测到您尚未登录,并将您发送到登录页面。
多么甜蜜!
我们在这个Angular系列文章中介绍了很多内容,所以让我们回顾一下我们学到的东西。
摘要
在第一篇文章中 ,我们学习了如何:
- 使用Angular CLI初始化我们的Todo应用程序
- 创建一个
Todo
类来代表单个Todo
- 创建
TodoDataService
服务以创建,更新和删除待办事项 - 使用
AppComponent
组件显示用户界面 - 将我们的应用程序部署到GitHub页面。
在第二篇文章中 ,我们重构了AppComponent
,将其大部分工作委托给:
-
TodoListComponent
以显示TodoListComponent
列表 -
TodoListItemComponent
以显示单个待办事项 - 一个
TodoListHeaderComponent
来创建一个新的待办事项 -
TodoListFooterComponent
来显示还剩下多少个TodoListFooterComponent
。
在第三篇文章中 ,我们学习了如何:
- 创建一个模拟REST API后端
- 将API URL存储为环境变量
- 创建一个
ApiService
与REST API通信 - 更新
TodoDataService
以使用新的ApiService
- 更新
AppComponent
以处理异步API调用 - 创建一个
ApiMockService
以避免在运行单元测试时进行真正的HTTP调用。
在第四篇文章中 ,我们了解到:
- 为什么应用程序可能需要路由
- 什么是JavaScript路由器
- 什么是Angular Router,它如何工作以及可以为您做什么
- 如何为我们的应用程序设置Angular路由器和配置路由
- 如何告诉Angular路由器在DOM中放置组件的位置
- 如何妥善处理未知URL
- 如何使用解析器让Angular路由器解析数据。
在第五篇文章中,我们了解到:
- Cookie和令牌之间的区别
- 如何创建
AuthService
来实现身份验证逻辑 - 如何创建一个
SessionService
来存储会话数据 - 如何使用AngularReact形式创建登录形式
- 如何创建路由防护以防止未经授权访问应用程序的某些部分
- 如何在HTTP请求中将用户令牌作为授权标头发送到您的API
- 为什么您永远不应该将用户的令牌发送给第三方。
如果您可以使用它,或者有任何疑问,请随时在下面的评论中告诉我们。
本文中的所有代码都可以在https://github.com/sitepoint-editors/angular-todo-app/tree/part-5上获得 。
有一个很棒的!
挑战
在当前状态下,浏览器刷新页面时,会话数据会丢失。
您能找出将会话数据保存在浏览器的sessionStorage或localStorage中需要什么吗?
在下面的评论中让我们知道您的想法。
祝好运!!
翻译自: https://www.sitepoint.com/angular-2-authentication-protecting-private-content/
angular私有模块