在本文中,我们将研究在MEAN堆栈中管理用户身份验证。 我们将使用最常见的MEAN架构,即使用Angular单页应用程序,该应用程序使用由Node,Express和MongoDB构建的REST API。
在考虑用户身份验证时,我们需要解决以下问题:
- 让用户注册
- 保存他们的数据,但不要直接存储他们的密码
- 让回访用户登录
- 在两次页面访问之间保持登录用户的会话有效
- 某些页面只能由登录用户查看
- 根据登录状态(例如“登录”按钮或“我的个人资料”按钮)将输出更改为屏幕。
在深入研究代码之前,让我们花一些时间来深入了解身份验证在MEAN堆栈中的工作方式。
MEAN堆栈认证流程
那么,认证在MEAN堆栈中是什么样的?
仍然保持较高的水平,这些是流程的组成部分:
- 用户数据存储在MongoDB中,密码经过哈希处理
- CRUD函数内置于Express API中-创建(注册),读取(登录,获取配置文件),更新,删除
- Angular应用程序调用API并处理响应
- Express API在注册或登录时会生成一个JSON Web令牌(JWT,发音为“ Jot”),并将其传递给Angular应用程序
- Angular应用程序存储JWT以维护用户的会话
- Angular应用程序在显示受保护的视图时检查JWT的有效性
- Angular应用程序在调用受保护的API路由时将JWT传递回Express。
为了保持浏览器中的会话状态,与Cookie相比,JWT是首选的。 使用服务器端应用程序时,Cookie更适合维护状态。
示例应用
GitHub上提供了本文的代码。 要运行该应用程序,您需要安装Node.js以及MongoDB。 (有关如何安装的说明,请参阅Mongo的官方文档-Windows,Linux和macOS )。
Angular应用
为了使本文中的示例保持简单,我们将从一个包含四个页面的Angular应用程序开始:
- 主页
- 注册页面
- 登录页面
- 个人资料页
这些页面非常基础,看起来像这样:
个人资料页面仅对经过身份验证的用户开放。 Angular应用程序的所有文件都在Angular CLI应用程序内一个名为/client
的文件夹中。
我们将使用Angular CLI来构建和运行本地服务器。 如果您不熟悉Angular CLI,请参考Angular 2教程:使用Angular CLI创建CRUD应用程序以开始使用。
REST API
我们还将从使用Node,Express和MongoDB构建的REST API的框架开始,使用Mongoose来管理模式。 该API具有以下三种路由:
-
/api/register
(POST)-处理新用户注册 -
/api/login
(POST)-处理返回的用户登录 -
/api/profile/USERID
(GET)-返回给定USERID
个人资料详细信息。
API的代码全部保存在Express应用程序内的另一个文件夹api
。 它保存了路由,控制器和模型,并且组织如下:
在这个起点上,每个控制器都简单地以一个确认响应,如下所示:
module.exports.register = function(req, res) {
console.log("Registering user: " + req.body.email);
res.status(200);
res.json({
"message" : "User registered: " + req.body.email
});
};
好的,让我们继续从数据库开始的代码。
使用Mongoose创建MongoDB数据架构
在/api/models/users.js
定义了一个简单的用户架构。 它定义了对电子邮件地址,名称,哈希和盐的需求。 将使用哈希和盐代替保存密码。 该email
设置为唯一,因为我们将其用作登录凭据。 这是模式:
var userSchema = new mongoose.Schema({
email: {
type: String,
unique: true,
required: true
},
name: {
type: String,
required: true
},
hash: String,
salt: String
});
在不保存密码的情况下管理密码
保存用户密码是一个很大的禁忌。 如果黑客获得了数据库的副本,则要确保他们无法使用该数据库登录帐户。 这是哈希和盐的来源。
盐是每个用户唯一的字符串。 哈希是通过将用户提供的密码和salt组合在一起,然后应用单向加密来创建的。 由于无法解密散列,因此验证用户身份的唯一方法是获取密码,将其与盐组合,然后再次对其进行加密。 如果此输出与哈希匹配,则密码必须正确。
要进行设置和密码检查,我们可以使用Mongoose模式方法。 这些本质上是您添加到架构的功能。 它们都将使用Node.js crypto
模块。
在users.js
模型文件的顶部,需要加密,以便我们可以使用它:
var crypto = require('crypto');
不需要安装任何东西,因为加密是Node的一部分。 加密本身有几种方法。 我们对randomBytes
可以创建随机盐,而pbkdf2Sync
可以创建哈希pbkdf2Sync
( Node.js API文档中有关Crypto的更多信息)。
设定密码
为了保存对密码的引用,我们可以在userSchema
模式上创建一个名为setPassword
的新方法,该方法接受password参数。 然后,该方法将使用crypto.randomBytes
设置盐,并使用crypto.pbkdf2Sync
设置哈希值:
userSchema.methods.setPassword = function(password){
this.salt = crypto.randomBytes(16).toString('hex');
this.hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex');
};
创建用户时,我们将使用此方法。 无需将密码保存到password
路径,我们可以将其传递给setPassword
函数以在用户文档中设置salt
和hash
路径。
检查密码
检查密码是一个类似的过程,但是我们已经从Mongoose模型中得到了盐。 这次,我们只想加密盐和密码,并查看输出是否与存储的哈希匹配。
向users.js
模型文件添加另一个新方法,称为validPassword
:
userSchema.methods.validPassword = function(password) {
var hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex');
return this.hash === hash;
};
生成JSON Web令牌(JWT)
Mongoose模型需要做的另一件事是生成JWT ,以便API可以将其发送为响应。 Mongoose方法在这里也是理想的,因为这意味着我们可以将代码保存在一个地方,并在需要时调用它。 当用户注册和用户登录时,我们需要调用它。
要创建JWT,我们将使用一个名为jsonwebtoken
的模块,该模块需要安装在应用程序中,因此请在命令行上运行该模块:
npm install jsonwebtoken --save
然后在users.js
模型文件中要求它:
var jwt = require('jsonwebtoken');
这个模块提供了一个sign
方法,我们可以使用它创建一个JWT,将要包含在令牌中的数据简单地传递给它,再加上一个哈希算法将使用的秘密。 数据应作为JavaScript对象发送,并在exp
属性中包含到期日期。
向userSchema
添加generateJwt
方法以返回JWT如下所示:
userSchema.methods.generateJwt = function() {
var expiry = new Date();
expiry.setDate(expiry.getDate() + 7);
return jwt.sign({
_id: this._id,
email: this.email,
name: this.name,
exp: parseInt(expiry.getTime() / 1000),
}, "MY_SECRET"); // DO NOT KEEP YOUR SECRET IN THE CODE!
};
注意:保守秘密是很重要的:只有原始服务器才知道它是什么。 最佳做法是将机密设置为环境变量,而不在源代码中包含它,特别是如果您的代码存储在版本控制中的某个位置。
这就是我们需要对数据库进行的所有操作。
设置护照以处理快速身份验证
Passport是一个Node模块,它简化了Express中处理身份验证的过程。 它提供了一个通用网关,可以处理许多不同的身份验证“策略”,例如使用Facebook,Twitter或Oauth登录。 我们将使用的策略称为“本地”,因为它使用本地存储的用户名和密码。
要使用Passport,首先安装它和策略,然后将它们保存在package.json
:
npm install passport --save
npm install passport-local --save
配置护照
在api
文件夹中,创建一个新的文件夹config
并在其中创建一个名为passport.js
的文件。 这是我们定义策略的地方。
在定义策略之前,此文件需要使用Passport,策略,猫鼬和User
模型:
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var mongoose = require('mongoose');
var User = mongoose.model('User');
对于本地策略,我们基本上只需要在User
模型上编写一个Mongoose查询。 该查询应找到具有指定电子邮件地址的用户,然后调用validPassword
方法以查看哈希是否匹配。 很简单
护照只有一种好奇心可以解决。 在内部,Passport的本地策略需要两段名为username
和password
的数据。 但是,我们使用email
作为唯一标识符,而不是username
。 可以在选项对象中使用策略定义中的usernameField
属性对其进行配置。 在那之后,它结束了Mongoose查询。
因此,整个策略定义将如下所示:
passport.use(new LocalStrategy({
usernameField: 'email'
},
function(username, password, done) {
User.findOne({ email: username }, function (err, user) {
if (err) { return done(err); }
// Return if user not found in database
if (!user) {
return done(null, false, {
message: 'User not found'
});
}
// Return if password is wrong
if (!user.validPassword(password)) {
return done(null, false, {
message: 'Password is wrong'
});
}
// If credentials are correct, return the user object
return done(null, user);
});
}
));
注意如何直接在user
实例上调用有效validPassword
模式方法。
现在,只需将Passport添加到应用程序中即可。 因此,在app.js
我们需要Passport模块,需要Passport配置并将Passport初始化为中间件。 将所有这些项目放置在app.js
中非常重要,因为它们需要按照一定的顺序排列。
应该在文件顶部使用Passport模块,并使用其他常规require
语句:
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var passport = require('passport');
在需要模型之后 ,应该需要配置,因为配置引用了模型。
require('./api/models/db');
require('./api/config/passport');
最后,在添加API路由之前,应将Passport初始化为Express中间件,因为这些路由是首次使用Passport。
app.use(passport.initialize());
app.use('/api', routesApi);
现在,我们已经设置了模式和Passport。 接下来,是时候将它们用于API的路由和控制器中了。
配置API端点
使用API,我们要做两件事:
- 使控制器正常运行
- 保护
/api/profile
路由,以便只有经过身份验证的用户才能访问它。
编码注册和登录API控制器
在示例应用程序中,注册和登录控制器位于/api/controllers/authentication.js
。 为了使控制器正常工作,该文件需要使用Passport,Mongoose和用户模型:
var passport = require('passport');
var mongoose = require('mongoose');
var User = mongoose.model('User');
注册API控制器
寄存器控制器需要执行以下操作:
- 从提交的表单中获取数据并创建一个新的Mongoose模型实例
- 调用我们之前创建的
setPassword
方法,将盐和哈希添加到实例中 - 将实例另存为记录到数据库
- 生成一个JWT
- 在JSON响应中发送JWT。
在代码中,所有这些看起来像这样:
module.exports.register = function(req, res) {
var user = new User();
user.name = req.body.name;
user.email = req.body.email;
user.setPassword(req.body.password);
user.save(function(err) {
var token;
token = user.generateJwt();
res.status(200);
res.json({
"token" : token
});
});
};
这利用了我们在Mongoose模式定义中创建的setPassword
和generateJwt
方法。 了解在架构中包含该代码如何使该控制器真正易于阅读和理解。
别忘了,实际上,此代码将具有许多错误陷阱,它们可以验证表单输入并在save
函数中捕获错误。 在这里省略它们以突出代码的主要功能。
登录API控制器
尽管可以(并且应该)事先添加一些验证来检查是否已发送必填字段,但登录控制器几乎将所有控制权移交给了Passport。
为了使Passport发挥其魔力并运行配置中定义的策略,我们需要调用authenticate
方法,如下所示。 此方法将使用三个可能的参数err
, user
和info
调用回调。 如果定义了user
,则可以使用它来生成要返回给浏览器的JWT:
module.exports.login = function(req, res) {
passport.authenticate('local', function(err, user, info){
var token;
// If Passport throws/catches an error
if (err) {
res.status(404).json(err);
return;
}
// If a user is found
if(user){
token = user.generateJwt();
res.status(200);
res.json({
"token" : token
});
} else {
// If user is not found
res.status(401).json(info);
}
})(req, res);
};
保护API路线
后端要做的最后一件事是确保只有经过身份验证的用户才能访问/api/profile
路由。 验证请求的方法是通过再次使用秘密来确保与它一起发送的JWT是真实的。 这就是为什么您应该将其保密而不是在代码中。
配置路由认证
首先,我们需要安装一个名为express-jwt
的中间件:
npm install express-jwt --save
然后,我们需要它并在定义路由的文件中对其进行配置。 在示例应用程序中,这是/api/routes/index.js
。 配置是在告诉它一个秘密的情况,以及(可选)告诉它的将在保存JWT的req
对象上创建的属性的名称。 我们将能够在与路线关联的控制器内使用此属性。 该属性的默认名称是user
,但这是我们的Mongoose User
模型实例的名称,因此我们将其设置为payload
以避免混淆:
var jwt = require('express-jwt');
var auth = jwt({
secret: 'MY_SECRET',
userProperty: 'payload'
});
同样, 不要在代码中保守秘密!
应用路由认证
要应用此中间件,只需在要保护的路由的中间引用该函数,如下所示:
router.get('/profile', auth, ctrlProfile.profileRead);
如果有人尝试在没有有效JWT的情况下立即访问该路由,则中间件将引发错误。 为确保我们的API正常运行,请通过将以下内容添加到主app.js文件的错误处理程序部分中来捕获此错误并返回401响应:
// error handlers
// Catch unauthorised errors
app.use(function (err, req, res, next) {
if (err.name === 'UnauthorizedError') {
res.status(401);
res.json({"message" : err.name + ": " + err.message});
}
});
使用路由验证
在此示例中,我们只希望人们能够查看自己的个人资料,因此我们从JWT获取用户ID并将其用于Mongoose查询中。
该路由的控制器位于/api/controllers/profile.js
。 该文件的全部内容如下所示:
var mongoose = require('mongoose');
var User = mongoose.model('User');
module.exports.profileRead = function(req, res) {
// If no user ID exists in the JWT return a 401
if (!req.payload._id) {
res.status(401).json({
"message" : "UnauthorizedError: private profile"
});
} else {
// Otherwise continue
User
.findById(req.payload._id)
.exec(function(err, user) {
res.status(200).json(user);
});
}
};
自然地,应该使用更多的错误捕获来充实它(例如,如果找不到用户),但是此代码段要简短一些,以演示该方法的关键点。
后端就是这样。 已配置数据库,我们具有用于注册和登录的API端点,这些端点生成和返回JWT,以及受保护的路由。 到前端!
创建角度认证服务
前端的大部分工作都可以放入Angular服务中,从而创建管理方法:
- 将JWT保存在本地存储中
- 从本地存储读取JWT
- 从本地存储中删除JWT
- 调用注册和登录API端点
- 检查用户当前是否已登录
- 从JWT获取登录用户的详细信息。
我们需要创建一个名为AuthenticationService
的新服务。 使用CLI,可以通过运行ng generate service authentication
并确保将其列在应用程序模块提供程序中来完成。 在示例应用程序中,该文件位于文件/client/src/app/authentication.service.ts
。
本地存储:保存,读取和删除JWT
为了使用户在localStorage
访问之间保持登录状态,我们在浏览器中使用localStorage
来保存JWT。 一种替代方法是使用sessionStorage
,它将仅在当前浏览器会话期间保留令牌。
首先,我们要创建一些接口来处理数据类型。 这对于检查应用程序的类型很有用。 该配置文件返回格式为UserDetails
的对象,并且登录和注册端点在请求期间期望TokenPayload
并返回TokenResponse
对象:
export interface UserDetails {
_id: string;
email: string;
name: string;
exp: number;
iat: number;
}
interface TokenResponse {
token: string;
}
export interface TokenPayload {
email: string;
password: string;
name?: string;
}
该服务使用来自Angular的HttpClient
服务向我们的服务器应用程序发出HTTP请求(稍后将使用),并使用Router
服务以编程方式导航。 我们必须将它们注入我们的服务构造函数中。
然后,我们定义了四种与JWT令牌交互的方法。 我们实现saveToken
来处理将令牌存储到localStorage
以及token
属性中的方法,使用getToken
方法从localStorage
或token
属性中检索token
,以及一个logout
函数,该函数从内存中删除JWT令牌并重定向到主页。
需要特别注意的是,如果您使用服务器端渲染,则此代码将不会运行,因为诸如localStorage
和window.atob
类的API不可用,并且Angular文档中提供了有关解决服务器端渲染的解决方案的详细信息。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators/map';
import { Router } from '@angular/router';
// Interfaces here
@Injectable()
export class AuthenticationService {
private token: string;
constructor(private http: HttpClient, private router: Router) {}
private saveToken(token: string): void {
localStorage.setItem('mean-token', token);
this.token = token;
}
private getToken(): string {
if (!this.token) {
this.token = localStorage.getItem('mean-token');
}
return this.token;
}
public logout(): void {
this.token = '';
window.localStorage.removeItem('mean-token');
this.router.navigateByUrl('/');
}
}
现在,让我们添加一种方法来检查此令牌以及令牌的有效性,以了解访问者是否已登录。
从JWT获取数据
当我们为JWT设置数据时(在generateJwt
Mongoose方法中),我们将到期日期包括在exp
属性中。 但是,如果您查看JWT,它似乎是一个随机字符串,如以下示例所示:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NWQ0MjNjMTUxMzcxMmNkMzE3YTRkYTciLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0NDA1NzA5NDUsImlhdCI6MTQzOTk2NjE0NX0.jS50GlmolxLoKrA_24LDKaW3vNaY94Y9EqYAFvsTiLg
那么,您如何阅读JWT?
JWT实际上是由三个单独的字符串组成,由点分隔.
。 这三个部分是:
- 标头 -编码的JSON对象,包含使用的类型和哈希算法
- 有效负载 -包含数据(令牌的实际主体)的编码JSON对象
- 签名 -使用服务器上设置的“秘密”,头和有效负载的加密哈希。
这是我们感兴趣的第二部分-有效负载。 请注意,这是编码而不是加密,这意味着我们可以对其进行解码 。
有一个称为atob()
的函数是现代浏览器所固有的,它将像这样解码Base64字符串。
因此,我们需要获取令牌的第二部分,对其进行解码并将其解析为JSON。 然后,我们可以检查有效期限是否尚未过去。
最后, getUserDetails
函数应返回UserDetails
类型或null
的对象,具体取决于是否找到有效的令牌。 放在一起,看起来像这样:
public getUserDetails(): UserDetails {
const token = this.getToken();
let payload;
if (token) {
payload = token.split('.')[1];
payload = window.atob(payload);
return JSON.parse(payload);
} else {
return null;
}
}
提供的用户详细信息包括有关用户名,电子邮件和令牌到期的信息,我们将使用这些信息来检查用户会话是否有效。
检查用户是否登录
将名为isLoggedIn
的新方法添加到服务。 它使用getUserDetails
方法从JWT令牌获取令牌详细信息,并检查到期时间是否尚未过去:
public isLoggedIn(): boolean {
const user = this.getUserDetails();
if (user) {
return user.exp > Date.now() / 1000;
} else {
return false;
}
}
如果令牌存在,则如果用户以布尔值登录,则该方法将返回。 现在,我们可以使用令牌进行授权来构造HTTP请求以加载数据。
构建API调用
为了方便进行API调用,请将request
方法添加到AuthenticationService
,它可以根据请求的特定类型构造并返回可观察到的正确HTTP请求。 这是一个私有方法,因为它仅由该服务使用,并且仅用于减少代码重复。 这将使用Angular HttpClient
服务; 如果还不存在,请记住将其注入AuthenticationService
:
private request(method: 'post'|'get', type: 'login'|'register'|'profile', user?: TokenPayload): Observable<any> {
let base;
if (method === 'post') {
base = this.http.post(`/api/${type}`, user);
} else {
base = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` }});
}
const request = base.pipe(
map((data: TokenResponse) => {
if (data.token) {
this.saveToken(data.token);
}
return data;
})
);
return request;
}
如果API登录或注册调用返回了令牌,则确实需要RxJS中的map
运算符来拦截并将令牌存储在服务中。 现在,我们可以实现公共方法来调用API。
调用注册和登录API端点
只需添加三种方法。 我们需要Angular应用程序和API之间的接口来调用登录和注册端点并保存返回的令牌或配置文件端点以获取用户详细信息:
public register(user: TokenPayload): Observable<any> {
return this.request('post', 'register', user);
}
public login(user: TokenPayload): Observable<any> {
return this.request('post', 'login', user);
}
public profile(): Observable<any> {
return this.request('get', 'profile');
}
每个方法都返回一个observable,它将处理我们需要进行的API调用之一的HTTP请求。 最终确定服务; 现在可以将所有内容捆绑在Angular应用中。
将身份验证应用于Angular App
我们可以通过多种方式在Angular应用程序中使用AuthenticationService
,以提供我们想要的体验:
- 填写注册和登录表格
- 更新导航以反映用户的状态
- 仅允许已登录的用户访问
/profile
路由 - 调用受保护的
/api/profile
API路由。
连接注册和登录控制器
我们将从查看注册和登录表单开始。
注册页面
注册表单的HTML已经存在,并且在字段上附加了NgModel
伪指令,所有伪指令都绑定到在credentials
控制器属性上设置的属性。 该表单还具有(submit)
事件绑定以处理提交。 在示例应用程序中,它位于/client/src/app/register/register.component.html
,如下所示:
<form (submit)="register()">
<div class="form-group">
<label for="name">Full name</label>
<input type="text" class="form-control" name="name" placeholder="Enter your name" [(ngModel)]="credentials.name">
</div>
<div class="form-group">
<label for="email">Email address</label>
<input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password">
</div>
<button type="submit" class="btn btn-default">Register!</button>
</form>
控制器中的第一项任务是确保我们的AuthenticationService
和Router
被注入并通过构造函数可用。 接下来,在表单提交的register
处理程序内,调用auth.register
,将表单中的凭据传递给它。
register
方法返回一个observable,我们需要订阅它才能触发请求。 可观察对象将发出成功或失败消息,如果有人成功注册,我们将设置应用程序将其重定向到个人资料页面或在控制台中记录错误。
在示例应用程序中,控制器位于/client/src/app/register/register.component.ts
,如下所示:
import { Component } from '@angular/core';
import { AuthenticationService, TokenPayload } from '../authentication.service';
import { Router } from '@angular/router';
@Component({
templateUrl: './register.component.html'
})
export class RegisterComponent {
credentials: TokenPayload = {
email: '',
name: '',
password: ''
};
constructor(private auth: AuthenticationService, private router: Router) {}
register() {
this.auth.register(this.credentials).subscribe(() => {
this.router.navigateByUrl('/profile');
}, (err) => {
console.error(err);
});
}
}
登录页面
登录页面在本质上与注册页面非常相似,但是在这种形式下,我们不需要输入名称,只需输入电子邮件和密码即可。 在示例应用程序中,它位于/client/src/app/login/login.component.html
,如下所示:
<form (submit)="login()">
<div class="form-group">
<label for="email">Email address</label>
<input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password">
</div>
<button type="submit" class="btn btn-default">Sign in!</button>
</form>
再次,我们具有表单提交处理程序,以及每个输入的NgModel
属性。 在控制器中,我们希望与注册控制器具有相同的功能,但是这次称为AuthenticationService
的login
方法。
在示例应用程序中,控制器位于/client/src/app/login/login.controller.ts
,如下所示:
import { Component } from '@angular/core';
import { AuthenticationService, TokenPayload } from '../authentication.service';
import { Router } from '@angular/router';
@Component({
templateUrl: './login.component.html'
})
export class LoginComponent {
credentials: TokenPayload = {
email: '',
password: ''
};
constructor(private auth: AuthenticationService, private router: Router) {}
login() {
this.auth.login(this.credentials).subscribe(() => {
this.router.navigateByUrl('/profile');
}, (err) => {
console.error(err);
});
}
}
现在,用户可以注册并登录该应用程序。 再次注意,表单中应进行更多验证,以确保在提交之前填写所有必填字段。 这些示例保持最少,以突出主要功能。
根据用户状态更改内容
在导航中,如果用户未登录,我们希望显示登录链接,如果用户已登录,则希望显示其用户名和指向个人资料页面的链接。在App组件中可以找到导航栏。
首先,我们来看一下App组件控制器。 我们可以将AuthenticationService
注入组件,然后直接在我们的模板中调用它。 在示例应用程序中,该文件位于/client/src/app/app.component.ts
,如下所示:
import { Component } from '@angular/core';
import { AuthenticationService } from './authentication.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(public auth: AuthenticationService) {}
}
这很简单,对吧? 现在,在关联的模板中,我们可以使用auth.isLoggedIn()
确定是显示登录链接还是显示配置文件链接。 要将用户名添加到配置文件链接中,我们可以访问auth.getUserDetails()?.name
的name属性。 请记住,这是从JWT获取数据的。 ?.
运算符是一种访问对象上可能未定义的属性而又不会引发错误的特殊方法。
在示例应用程序中,文件位于/client/src/app/app.component.html
,更新的部分如下所示:
<ul class="nav navbar-nav navbar-right">
<li *ngIf="!auth.isLoggedIn()"><a routerLink="/login">Sign in</a></li>
<li *ngIf="auth.isLoggedIn()"><a routerLink="/profile">{{ auth.getUserDetails()?.name }}</a></li>
<li *ngIf="auth.isLoggedIn()"><a (click)="auth.logout()">Logout</a></li>
</ul>
保护仅登录用户的路由
在这一步中,我们将了解如何通过保护/profile
路径使仅登录用户可以访问的路由。
Angular允许您定义路由防护,可以在路由生命周期的多个点运行检查,以确定是否可以加载路由。 仅当用户登录时,我们CanActivate
使用CanActivate
挂钩告诉Angular加载配置文件路由。
为此,我们需要创建一个路由防护服务, ng generate service auth-guard
。 它必须实现CanActivate
接口以及关联的canActivate
方法。 此方法从AuthenticationService.isLoggedIn
方法返回一个布尔值(基本上检查是否找到了令牌,并且仍然有效),并且如果用户无效,还将它们重定向到主页:
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { AuthenticationService } from './authentication.service';
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private auth: AuthenticationService, private router: Router) {}
canActivate() {
if (!this.auth.isLoggedIn()) {
this.router.navigateByUrl('/');
return false;
}
return true;
}
}
要启用此防护,我们必须在路由配置中对其进行声明。 有一个名为canActivate
的属性,该属性接受在激活路由之前应调用的服务数组。 确保您还在App NgModule
的providers
数组中声明了这些服务。 路由在App模块中定义,该模块包含如您在此处看到的路由:
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'profile', component: ProfileComponent, canActivate: [AuthGuardService] }
];
有了该路由保护器之后,现在,如果未经身份验证的用户尝试访问配置文件页面,Angular将取消路由更改并重定向到主页,从而保护其免受未经身份验证的用户的侵害。
调用受保护的API路由
/api/profile
路由已设置为检查请求中的JWT。 否则,它将返回401未经授权的错误。
要将令牌传递给API,需要将其作为请求的标头(称为Authorization
。 以下代码段显示了主要的数据服务功能,以及发送令牌所需的格式。 AuthenticationService
已经处理了此问题,但是您可以在/client/src/app/authentication.service.ts
找到它。
base = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` }});
请记住,通过使用仅发行服务器已知的机密,后端代码将在发出请求时验证令牌是真实的。
要在配置文件页面中使用此功能,我们只需要在示例应用程序的/client/src/app/profile/profile.component.ts
中更新控制器/client/src/app/profile/profile.component.ts
。 当API返回一些应与UserDetails
接口匹配的数据时,它将填充details
属性。
import { Component } from '@angular/core';
import { AuthenticationService, UserDetails } from '../authentication.service';
@Component({
templateUrl: './profile.component.html'
})
export class ProfileComponent {
details: UserDetails;
constructor(private auth: AuthenticationService) {}
ngOnInit() {
this.auth.profile().subscribe(user => {
this.details = user;
}, (err) => {
console.error(err);
});
}
}
然后,当然,这只是在视图中更新绑定的一种情况( /client/src/app/profile/profile.component.html
)。 同样, ?.
是绑定第一个渲染器上不存在的属性的安全操作符(因为必须先加载数据)。
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">Full name</label>
<p class="form-control-static">{{ details?.name }}</p>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Email</label>
<p class="form-control-static">{{ details?.email }}</p>
</div>
</div>
这是登录后的最终个人资料页面:
从保护API路由和管理用户详细信息到使用JWT和保护路由,这就是在MEAN堆栈中管理身份验证的方法。 如果您在自己的一个应用中实现了这样的身份验证系统,并且有任何提示,技巧或建议,请务必在下面的评论中分享它们!
From: https://www.sitepoint.com/user-authentication-mean-stack/