首頁  >  文章  >  CMS教程  >  使用Angular和Node進行基於令牌的身份驗證

使用Angular和Node進行基於令牌的身份驗證

王林
王林原創
2023-09-01 14:01:061264瀏覽

使用Angular和Node進行基於令牌的身份驗證

身份驗證是任何 Web 應用程式中最重要的部分之一。本教程討論基於令牌的身份驗證系統以及它們與傳統登入系統的差異。在本教程結束時,您將看到一個用 Angular 和 Node.js 編寫的完整工作演示。

傳統身分驗證系統

在繼續基於令牌的身份驗證系統之前,讓我們先來看看傳統的身份驗證系統。

  1. 使用者在登入表單中提供使用者名稱密碼,然後點擊登入
  2. 發出請求後,透過查詢資料庫在後端驗證使用者。如果請求有效,則使用從資料庫中獲取的使用者資訊建立會話,然後在回應頭中傳回會話訊息,以便將會話ID儲存在瀏覽器中。
  3. 提供用於存取應用程式中受限端點的會話資訊。
  4. 如果會話資訊有效,則讓使用者存取指定端點,並使用呈現的 HTML 內容進行回應。

使用Angular和Node進行基於令牌的身份驗證

#到目前為止一切都很好。 Web 應用程式運作良好,並且能夠對使用者進行身份驗證,以便他們可以存取受限端點。但是,當您想為您的應用程式開發另一個客戶端(例如 Android 用戶端)時會發生什麼情況?您是否能夠使用目前的應用程式來驗證行動用戶端並提供受限制的內容?就目前情況而言,沒有。造成這種情況的主要原因有兩個:

  1. 會話和 Cookie 對於行動應用程式沒有意義。您無法與行動用戶端分享在伺服器端建立的會話或 Cookie。
  2. 在目前應用程式中,傳回呈現的 HTML。在行動用戶端中,您需要包含 JSON 或 XML 等內容作為回應。

在這種情況下,您需要一個獨立於客戶端的應用程式。

基於令牌的身份驗證

在基於令牌的身份驗證中,不會使用 cookie 和會話。令牌將用於對向伺服器發出的每個請求進行使用者身份驗證。讓我們使用基於令牌的身份驗證重新設計第一個場景。

它將使用以下控制流程:

  1. 使用者在登入表單中提供使用者名稱密碼,然後點擊登入
  2. 發出請求後,透過在資料庫中查詢來驗證後端的使用者。如果請求有效,則使用從資料庫獲取的使用者資訊建立令牌,然後在回應標頭中傳回該訊息,以便我們可以將令牌瀏覽器儲存在本地儲存中。
  3. 在每個請求標頭中提供令牌訊息,以存取應用程式中的受限端點。
  4. 如果從請求標頭資訊中取得的令牌有效,則讓使用者存取指定端點,並使用 JSON 或 XML 進行回應。

在這種情況下,我們沒有回傳會話或cookie,也沒有回傳任何HTML內容。這意味著我們可以將此架構用於任何客戶端的特定應用程式。您可以看到下面的架構架構:

使用Angular和Node進行基於令牌的身份驗證

#那麼這個 JWT 是什麼?

JWT

JWT 代表 JSON Web 令牌,是授權標頭中使用的令牌格式。此令牌可協助您以安全的方式設計兩個系統之間的通訊。出於本教學的目的,我們將 JWT 重新表述為「不記名令牌」。不記名令牌由三個部分組成:標頭、負載和簽名。

  • 標頭是令牌中保存令牌類型和加密方法的部分,也是使用 Base-64 進行加密的。
  • 有效負載包含資訊。您可以輸入任何類型的數據,例如使用者資訊、產品資訊等,所有這些數據都使用 Base-64 加密進行儲存。
  • 簽章由標頭、負載和金鑰的組合組成。密鑰必須安全地保存在伺服器端。

您可以在下面看到 JWT 架構和範例令牌:

使用Angular和Node進行基於令牌的身份驗證

#您不需要實作不記名令牌產生器,因為您可以找到多種語言的已建立套件。您可以在下面看到其中一些:

##PHPhttp://github.com/firebase/php-jwtJavahttp://github.com/auth0/java-jwt紅寶石https://github.com/jwt/ruby-jwt ###。網### https://github.com/auth0/java-jwthttp://github.com/progrium/pyjwt/

一個實際範例

介紹了有關基於令牌的身份驗證的一些基本資訊後,我們現在可以繼續討論一個實際範例。看看下面的架構,然後我們將更詳細地分析它:

使用Angular和Node進行基於令牌的身份驗證

#
  1. 多個用戶端(例如網路應用程式或行動用戶端)出於特定目的向 API 發出請求。
  2. 要求是向 https://api.yourexampleapp.com 等服務發出的。如果很多人使用該應用程序,則可能需要多個伺服器來提供請求的操作。
  3. 這裡,負載平衡器用於平衡請求,以最適合後端的應用程式伺服器。當您向 https://api.yourexampleapp.com 發出請求時,負載平衡器會先處理請求,然後會將客戶端重新導向到特定伺服器。
  4. 有一個應用程序,並且該應用程式部署到多台伺服器(server-1、server-2、...、server-n)。每當向 https://api.yourexampleapp.com 發出請求時,後端應用程式都會攔截請求標頭並從授權標頭中提取令牌資訊。將使用此令牌進行資料庫查詢。如果此令牌有效且具有存取所請求端點所需的權限,則它將繼續。如果沒有,它將傳回 403 回應代碼(表示禁止狀態)。

優點

基於令牌的身份驗證具有解決嚴重問題的多個優點。以下是其中的一些:

獨立於客戶端的服務

在基於令牌的身份驗證中,令牌透過請求標頭傳輸,而不是將身份驗證資訊保留在會話或 cookie 中。這意味著沒有狀態。您可以從任何類型的可以發出 HTTP 請求的客戶端向伺服器發送請求。

內容傳遞網路 (CDN)

在目前的大多數 Web 應用程式中,視圖在後端呈現,HTML 內容返回瀏覽器。前端邏輯依賴後端程式碼。

沒有必要建立這樣的依賴關係。這帶來了幾個問題。例如,如果您正在與實作前端 HTML、CSS 和 JavaScript 的設計機構合作,您需要將該前端程式碼遷移到後端程式碼中,以便進行一些渲染或填滿操作。一段時間後,您呈現的 HTML 內容將與程式碼機構實現的內容有很大不同。

在基於令牌的身份驗證中,您可以與後端程式碼分開開發前端專案。您的後端程式碼將傳回 JSON 回應,而不是渲染的 HTML,而且您可以將前端程式碼的縮小、gzip 版本放入 CDN 中。當您造訪網頁時,HTML 內容將從 CDN 提供,並且頁面內容將由 API 服務使用授權標頭中的令牌填充。

CSRF 是現代網路安全的一個主要問題,因為它不會檢查請求來源是否可信。為了解決這個問題,使用令牌池在每個表單貼文上發送該令牌。在基於令牌的身份驗證中,令牌用於授權標頭,而 CSRF 不包含該資訊。

持久令牌儲存

當應用程式中進行會話讀取、寫入或刪除操作時,它會在作業系統的 temp 資料夾中進行檔案操作,至少第一次是這樣。假設您有多個伺服器,並且在第一台伺服器上建立了一個會話。當您發出另一個請求並且您的請求落入另一台伺服器時,會話資訊將不存在並且將得到「未經授權」的回應。我知道,你可以透過黏性會話來解決這個問題。然而,在基於令牌的認證中,這種情況自然就解決了。不存在黏性會話問題,因為請求令牌在任何伺服器上的每個請求上都會被攔截。

這些是基於令牌的身份驗證和通訊的最常見優點。關於基於令牌的身份驗證的理論和架構討論就到此結束。是時候看一個實際例子了。

範例應用程式

您將看到兩個應用程式來演示基於令牌的身份驗證:

  1. 基於令牌的身份驗證後端
  2. 基於令牌的身份驗證前端

在後端專案中,會有服務的實現,服務結果將是JSON格式。服務中沒有返回視圖。在前端專案中,將有一個用於前端 HTML 的 Angular 項目,然後前端應用程式將由 Angular 服務填充,以向後端服務發出請求。

基於令牌的身份驗證後端

在後端專案中,主要有三個檔案:

  • package.json 用於依賴管理。
  • models/User.js 包含一個使用者模型,用於對使用者進行資料庫操作。
  • server.js 用於專案引導和請求處理。

就是這樣!這個項目非常簡單,因此您無需深入研究即可輕鬆理解主要概念。

{
    "name": "angular-restful-auth",
    "version": "0.0.1",
    "dependencies": {
        "body-parser": "^1.20.2",
        "express": "4.x",
        "express-jwt": "8.4.1",
        "jsonwebtoken": "9.0.0",
        "mongoose": "7.3.1",
        "morgan": "latest"
    },
    "engines": {
        "node": ">=0.10.0"
    }
}
 

package.json 包含專案的依賴: express 用於MVC,body-parser 用於模擬post Node. js 中的請求處理,morgan 用於請求日誌記錄,mongoose 用於我們的ORM 框架連接到MongoDB,和jsonwebtoken 用於使用我們的使用者模型建立JWT 令牌。還有一個名為 engines 的屬性,表示該專案是使用 Node.js 版本 >= 0.10.0 製作的。這對於 Heroku 等 PaaS 服務很有用。我們還將在另一節中討論主題。

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
  email: String,
  password: String,
  token: String
});

module.exports = mongoose.model('User', UserSchema);
 

我們說過我們將使用使用者模型有效負載產生令牌。這個模型幫助我們對MongoDB進行使用者操作。在User.js中,定義了使用者模式並使用貓鼬模型建立了使用者模型。該模型已準備好進行資料庫操作。

我們的依賴關係已經定義,我們的使用者模型也已經定義,所以現在讓我們將所有這些組合起來建構一個用於處理特定請求的服務。

// Required Modules
const express    = require("express");
const morgan     = require("morgan");
const bodyParser = require("body-parser");
const jwt        = require("jsonwebtoken");
const mongoose   = require("mongoose");
const app        = express();
 

在 Node.js 中,您可以使用 require 在專案中包含模組。首先,我們需要將必要的模組導入到專案中:

const port = process.env.PORT || 3001;
const User     = require('./models/User');

// Connect to DB
mongoose.connect(process.env.MONGO_URL);
 

我們的服務將透過特定連接埠提供服務。如果系統環境變數中定義了任何連接埠變量,則可以使用它,或者我們定義了連接埠 3001。之後,包含了User模型,並建立了資料庫連接,以進行一些使用者操作。不要忘記為資料庫連接 URL 定義一個環境變數 MONGO_URL

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
app.use(function(req, res, next) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization');
    next();
});

 

在上面的部分中,我們使用 Express 進行了一些配置來模擬 Node 中的 HTTP 請求處理。我們允許來自不同網域的請求,以便開發獨立於客戶端的系統。如果您不允許這樣做,您將在網頁瀏覽器中觸發 CORS(跨來源請求共用)錯誤。

  • Access-Control-Allow-Origin 允许所有域。
  • 您可以向此服务发送 POSTGET 请求。
  • X-Requested-Withcontent-type 标头是允许的。
app.post('/authenticate', async function(req, res) {
    try {
      const user = await User.findOne({ email: req.body.email, password: req.body.password }).exec();
      if (user) {
        res.json({
          type: true,
          data: user,
          token: user.token
        });
      } else {
        res.json({
          type: false,
          data: "Incorrect email/password"
        });
      }
    } catch (err) {
      res.json({
        type: false,
        data: "Error occurred: " + err
      });
    }
  });
 

我们已经导入了所有必需的模块并定义了我们的配置,所以现在是时候定义请求处理程序了。在上面的代码中,每当你使用用户名和密码向 /authenticate 发出 POST 请求时,你都会得到一个 JWT 令牌。首先,使用用户名和密码处理数据库查询。如果用户存在,则用户数据将与其令牌一起返回。但是如果没有与用户名和/或密码匹配的用户怎么办?

 app.post('/signin', async function(req, res) {
    try {
      const existingUser = await User.findOne({ email: req.body.email }).exec();
      if (existingUser) {
        res.json({
          type: false,
          data: "User already exists!"
        });
      } else {
        const userModel = new User();
        userModel.email = req.body.email;
        userModel.password = req.body.password;
        const savedUser = await userModel.save();
        savedUser.token = jwt.sign(savedUser.toObject(), process.env.JWT_SECRET);
        const updatedUser = await savedUser.save();
        res.json({
          type: true,
          data: updatedUser,
          token: updatedUser.token
        });
      }
    } catch (err) {
      res.json({
        type: false,
        data: "Error occurred: " + err
      });
    }
  });
 

当您使用用户名和密码向 /signin 发出 POST 请求时,将使用发布的用户信息创建一个新用户。在 14th 行,您可以看到使用 jsonwebtoken 模块生成了一个新的 JSON 令牌,该令牌已分配给 jwt 变量。认证部分没问题。如果我们尝试访问受限端点怎么办?我们如何设法访问该端点?

app.get('/me', ensureAuthorized, async function(req, res) {
    try {
      const user = await User.findOne({ token: req.token }).exec();
      res.json({
        type: true,
        data: user
      });
    } catch (err) {
      res.json({
        type: false,
        data: "Error occurred: " + err
      });
    }
  });
 

当您向 /me 发出 GET 请求时,您将获得当前用户信息,但为了继续请求的端点,确保Authorized函数将被执行。

function ensureAuthorized(req, res, next) {
    var bearerToken;
    var bearerHeader = req.headers["authorization"];
    if (typeof bearerHeader !== 'undefined') {
        var bearer = bearerHeader.split(" ");
        bearerToken = bearer[1];
        req.token = bearerToken;
        next();
    } else {
        res.send(403);
    }
}
 

在该函数中,拦截请求头,并提取authorization头。如果此标头中存在承载令牌,则该令牌将分配给 req.token 以便在整个请求中使用,并且可以使用 next( )。如果令牌不存在,您将收到 403(禁止)响应。让我们回到处理程序 /me,并使用 req.token 使用此令牌获取用户数据。每当您创建新用户时,都会生成一个令牌并将其保存在数据库的用户模型中。这些令牌是独一无二的。

对于这个简单的项目,我们只有三个处理程序。之后,您将看到:

process.on('uncaughtException', function(err) {
    console.log(err);
});
 

如果发生错误,Node.js 应用程序可能会崩溃。使用上面的代码,可以防止崩溃,并在控制台中打印错误日志。最后,我们可以使用以下代码片段启动服务器。

// Start Server
app.listen(port, function () {
    console.log( "Express server listening on port " + port);
});
 

总结一下:

  • 模块已导入。
  • 配置已完成。
  • 已定义请求处理程序。
  • 定义中间件是为了拦截受限端点。
  • 服务器已启动。

我们已经完成了后端服务。为了让多个客户端可以使用它,您可以将这个简单的服务器应用程序部署到您的服务器上,或者也可以部署在 Heroku 中。项目根文件夹中有一个名为 Procfile 的文件。让我们在 Heroku 中部署我们的服务。

Heroku 部署

您可以从此 GitHub 存储库克隆后端项目。

我不会讨论如何在 Heroku 中创建应用程序;如果您之前没有创建过 Heroku 应用程序,可以参考这篇文章来创建 Heroku 应用程序。创建 Heroku 应用程序后,您可以使用以下命令将目标添加到当前项目:

git remote add heroku <your_heroku_git_url>

现在您已经克隆了一个项目并添加了一个目标。在 git addgit commit 之后,您可以通过执行 git push heroku master 将代码推送到 Heroku。当您成功推送项目时,Heroku 将执行 npm install 命令将依赖项下载到 Heroku 上的 temp 文件夹中。之后,它将启动您的应用程序,您可以使用 HTTP 协议访问您的服务。

基于令牌的-auth-frontend

在前端项目中,您将看到一个 Angular 项目。在这里,我只提及前端项目中的主要部分,因为 Angular 不是一个教程可以涵盖的内容。

您可以从此 GitHub 存储库克隆该项目。在此项目中,您将看到以下文件夹结构:

使用Angular和Node進行基於令牌的身份驗證

我们拥有三个组件——注册、配置文件和登录——以及一个身份验证服务。

您的app.component.html 如下所示:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
        <div class="container-fluid">
          <a class="navbar-brand" href="#">Home</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
              
              <li class="nav-item"><a class="nav-link" routerLink="/profile">Me</a></li>
              <li class="nav-item"><a class="nav-link" routerLink="/login">Signin</a></li>
              <li class="nav-item"><a class="nav-link" routerLink="/signup">Signup</a></li>
              <li class="nav-item"><a class="nav-link" (click)="logout()">Logout</a></li>
            </ul>
          </div>
        </div>
      </nav>

    <div class="container">
        <router-outlet></router-outlet>
    </div> 

  </body>
</html>
 

在主组件文件中,<router-outlet></router-outlet> 定义各个组件的路由。

auth.service.ts 文件中,我们定义 AuthService 类,该类通过 API 调用来处理身份验证,以登录、验证 Node.js 应用程序的 API 端点。

import { Injectable } from '@angular/core';
import { HttpClient,HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private apiUrl = 'your_node_app_url';
  public token: string ='';


  constructor(private http: HttpClient) {
    
  }


  signin(username: string, password: string): Observable<any> {
    const data = { username, password };
    return this.http.post(`${this.apiUrl}/signin`, data);
  }

 

  authenticate(email: string, password: string): Observable<any> {
    const data = { email, password };
    console.log(data)

    return this.http.post(`${this.apiUrl}/authenticate`, data)
      .pipe(
        tap((response:any) => {
          this.token = response.data.token; // Store the received token
          localStorage.setItem('token',this.token)
          console.log(this.token)
        })
      );
  }

  profile(): Observable<any> {
    const headers = this.createHeaders();
    return this.http.get(`${this.apiUrl}/me`,{ headers });
  }


  private createHeaders(): HttpHeaders {
    let headers = new HttpHeaders({
      'Content-Type': 'application/json',
    });

    if (this.token) {
      headers = headers.append('Authorization', `Bearer ${this.token}`);
    }

    return headers;
  }

  logout(): void {
    
    localStorage.removeItem('token');
  }
 
  
}


authenticate() 方法中,我们向 API 发送 POST 请求并对用户进行身份验证。从响应中,我们提取令牌并将其存储在服务的 this.token 属性和浏览器的 localStorage 中,然后将响应作为 Observable 返回。

profile() 方法中,我们通过在 Authorization 标头中包含令牌来发出 GET 请求以获取用户详细信息。

createHeaders() 方法在发出经过身份验证的 API 请求时创建包含身份验证令牌的 HTTP 标头。当用户拥有有效令牌时,它会添加一个授权标头。该令牌允许后端 API 对用户进行身份验证。

如果身份验证成功,用户令牌将存储在本地存储中以供后续请求使用。该令牌也可供所有组件使用。如果身份验证失败,我们会显示一条错误消息。

不要忘记将服务 URL 放入上面代码中的 baseUrl 中。当您将服务部署到 Heroku 时,您将获得类似 appname.herokuapp.com 的服务 URL。在上面的代码中,您将设置 var baseUrl = "appname.herokuapp.com"

注销功能从本地存储中删除令牌。

signup.component.ts 文件中,我们实现了 signup () 方法,该方法获取用户提交的电子邮件和密码并创建一个新用户。

import { Component } from '@angular/core';
import { AuthService } from '../auth.service';



@Component({
  selector: 'app-signup',
  templateUrl: './signup.component.html',
  styleUrls: ['./signup.component.css']
})
export class SignupComponent {
  password: string = '';
  email: string = '';
  

  constructor(private authService:AuthService){}

  signup(): void {
    this.authService.signin(this.email, this.password).subscribe(
      (response) => {
        // success response
        console.log('Authentication successful', response);
       
      },
      (error) => {
        // error response
        console.error('Authentication error', error);
      }
    );
  }
}
  login.component.ts 文件看起来与注册组件类似。  
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';



@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
    
  email: string = '';
  password: string = '';

  constructor(private authService: AuthService) {}

  login(): void {
    this.authService.authenticate(this.email, this.password).subscribe(
      (response) => {
        // success response
        console.log('Signin successful', response);
       
      },
      (error) => {
        // error response
        console.error('Signin error', error);
      }
    );
  }
}

配置文件组件使用用户令牌来获取用户的详细信息。每当您向后端的服务发出请求时,都需要将此令牌放入标头中。 profile.component.ts 如下所示:

import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})

export class ProfileComponent {
  myDetails: any;

  constructor(private authService: AuthService) { }

  ngOnInit(): void {
    this.getProfileData();
  }
  getProfileData(): void {
    this.authService.me().subscribe(
      (response: any) => {
        this.myDetails = response;
        console.log('User Data:', this.myDetails);
      },
      (error: any) => {
        console.error('Error retrieving profile data');
      }
    );
  }
 

在上面的代码中,每个请求都会被拦截,并在标头中放入授权标头和值。然后,我们将用户详细信息传递到 profile.component.html 模板。

<h2>User profile </h2>

<div class="row">
    <div class="col-lg-12">
        <p>{{myDetails.data.id}}</p>
        <p>{{myDetails.data.email}}</p>
    </div>
</div>

最后,我们在 app.routing.module.ts 中定义路由。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ProfileComponent } from './profile/profile.component';
import { SignupComponent } from './signup/signup.component';

const routes: Routes = [
  {path:'signup' , component:SignupComponent},
  {path:'login' , component:LoginComponent},
  { path: 'profile', component: ProfileComponent },
];

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

从上面的代码中您可以很容易地理解,当您转到/时,将呈现app.component.html页面。另一个例子:如果您转到/signup,则会呈现signup.component.html。这个渲染操作将在浏览器中完成,而不是在服务器端。

结论

基于令牌的身份验证系统可帮助您在开发独立于客户端的服务时构建身份验证/授权系统。通过使用这项技术,您将只需专注于您的服务(或 API)。

身份验证/授权部分将由基于令牌的身份验证系统作为服务前面的一层进行处理。您可以从任何客户端(例如网络浏览器、Android、iOS 或桌面客户端)访问和使用服务。

Node.js https://github.com/auth0/node-jsonwebtoken
#
#
#
# ###Python###

以上是使用Angular和Node進行基於令牌的身份驗證的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn