mean堆栈_使用MEAN堆栈进行用户身份验证

mean堆栈

在本文中,我们将研究在MEAN堆栈中管理用户身份验证。 我们将使用最常见的MEAN架构,即使用Angular单页应用程序,该应用程序使用由Node,Express和MongoDB构建的REST API。

在考虑用户身份验证时,我们需要解决以下问题:

  1. 让用户注册
  2. 保存他们的数据,但永远不要直接存储他们的密码
  3. 让回访用户登录
  4. 在两次页面访问之间保持登录用户的会话有效
  5. 某些页面只能由登录用户查看
  6. 根据登录状态(例如“登录”按钮或“我的个人资料”按钮)将输出更改为屏幕。

在深入研究代码之前,让我们花一些时间来深入了解身份验证在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应用程序开始:

  1. 主页
  2. 注册页面
  3. 登录页面
  4. 个人资料页

这些页面非常基础,看起来像这样:

该应用程序的屏幕截图

个人资料页面仅对经过身份验证的用户开放。 Angular应用程序的所有文件都在Angular CLI应用程序内一个名为/client的文件夹中。

我们将使用Angular CLI来构建和运行本地服务器。 如果您不熟悉Angular CLI,请参阅Angular 2教程:使用Angular CLI创建CRUD应用程序以开始使用。

REST API

我们还将以使用Mongoose管理模式的,由Node,Express和MongoDB构建的REST API的框架开始。 该API具有以下三种路由:

  1. /api/register (POST)-处理新用户注册
  2. /api/login (POST)-处理返回的用户登录
  3. /api/profile/USERID (GET)-返回给定USERID个人资料详细信息。

API的代码全部保存在Express应用程序内的另一个文件夹api 。 它保存了路由,控制器和模型,并且组织如下:

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
});

在不保存密码的情况下管理密码

保存用户密码是一个很大的禁忌。 如果黑客获得了数据库的副本,则要确保他们无法使用该数据库登录帐户。 这是哈希和盐的来源。

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

盐是每个用户唯一的字符串。 哈希是通过将用户提供的密码和salt组合在一起,然后应用单向加密来创建的。 由于无法解密散列,因此验证用户身份的唯一方法是获取密码,将其与盐组合,然后再次对其进行加密。 如果此输出与哈希匹配,则密码必须正确。

要进行设置和密码检查,我们可以使用Mongoose模式方法。 这些本质上是您添加到架构的功能。 它们都将使用Node.js crypto模块。

users.js模型文件的顶部,需要加密,以便我们可以使用它:

var crypto = require('crypto');

不需要安装任何东西,因为加密是Node的一部分。 加密本身有几种方法。 我们对randomBytes可以创建随机盐,而pbkdf2Sync可以创建哈希pbkdf2SyncNode.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函数,以在用户文档中设置salthash路径。

检查密码

检查密码是一个类似的过程,但是我们已经从猫鼬模型中得到了盐。 这次,我们只想加密盐和密码,并查看输出是否与存储的哈希匹配。

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的本地策略需要两个名为usernamepassword的数据。 但是,我们使用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​​,我们有两件事要做:

  1. 使控制器正常运行
  2. 保护/api/profile路由,以便只有经过身份验证的用户才能访问它。

编码注册和登录API控制器

在示例应用程序中,注册和登录控制器位于/api/controllers/authentication.js 。 为了使控制器正常工作,该文件需要使用Passport,Mongoose和用户模型:

var passport = require('passport');
var mongoose = require('mongoose');
var User = mongoose.model('User');
注册API控制器

寄存器控制器需要执行以下操作:

  1. 从提交的表单中获取数据并创建一个新的Mongoose模型实例
  2. 调用我们之前创建的setPassword方法,将salt和哈希值添加到实例中
  3. 将实例保存为记录到数据库
  4. 生成一个JWT
  5. 在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模式定义中创建的setPasswordgenerateJwt方法。 了解在架构中包含该代码如何使该控制器真正易于阅读和理解。

别忘了,实际上,此代码将具有许多错误陷阱,它们可以验证表单输入并在save函数中捕获错误。 这里省略它们以突出代码的主要功能。

登录API控制器

尽管可以(并且应该)事先添加一些验证来检查是否已发送必填字段,但登录控制器几乎将所有控制权移交给了Passport。

为了使Passport发挥其魔力并运行配置中定义的策略,我们需要调用authenticate方法,如下所示。 此方法将使用三个可能的参数erruserinfo调用回调。 如果定义了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方法从localStoragetoken属性中检索token ,以及一个logout函数,该函数从内存中删除JWT令牌并重定向到主页。

需要特别注意的是,如果您使用服务器端渲染,则此代码不会运行,因为诸如localStoragewindow.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实际上是由三个单独的字符串组成,由点分隔. 。 这三个部分是:

  1. 标头 -编码的JSON对象,包含使用的类型和哈希算法
  2. 有效载荷 —包含数据(令牌的真实主体)的编码JSON对象
  3. 签名 -使用服务器上设置的“秘密”,头和有效负载的加密哈希。

这是我们对此感兴趣的第二部分-有效负载。 请注意,它是编码而不是加密的,这意味着我们可以对其进行解码

有一个称为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 ,以提供我们所追求的体验:

  1. 填写注册和登录表格
  2. 更新导航以反映用户的状态
  3. 仅允许已登录的用户访问/profile路由
  4. 调用受保护的/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>

控制器中的第一项任务是确保我们的AuthenticationServiceRouter被注入并通过构造函数可用。 接下来,在表单提交的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属性。 在控制器中,我们希望与注册控制器具有相同的功能,但是这次称为AuthenticationServicelogin方法。

在示例应用程序中,控制器位于/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的属性,该属性canActivate在激活路由之前应调用的服务数组。 确保您还在App NgModuleproviders数组中声明了这些服务。 路由是在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堆栈中管理身份验证的方法。 如果您在自己的一个应用中实现了这样的身份验证系统,并且有任何提示,技巧或建议,请务必在下面的评论中分享它们!

翻译自: https://www.sitepoint.com/user-authentication-mean-stack/

mean堆栈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值