微服务身份验证策略:理论到实践 理论 实践

2025-06-08

微服务身份验证策略:理论到实践

理论

实践

在本文中,我们将介绍实现身份验证微服务的常见方法。

我们将分为两部分:

1.理论部分讨论OpenID Connect,OAuth 2.0,JWT等。

在这里,我尝试节省您浏览网页的时间,并为您提供开始编码所需了解的所有基础知识。

2. 实践部分,我们将实现两个 Node.js 微服务,一个负责通过 Google 登录进行用户身份验证,另一个负责使用前一个服务创建的令牌来向用户发送问候语。此外,我们还添加了一个 React.JS 应用来与这两个服务交互。

理论

什么是身份验证?

身份验证是用户在询问“你是谁?”时给出的答案。为了让我们相信用户,他们需要经过一个提供证据的过程。

例如 - 通过提供用户名和密码或使用社交登录提供商。

什么是授权?

当我们已经知道用户是谁时,授权通常是相关的,因此用户已经过身份验证(除非我们允许匿名访问,但我们不会在这里讨论该用例)。

我们的用户想要在我们的系统中执行某些操作,而检查他们是否被允许执行该操作的过程称为授权。

(注意:我们在身份验证文章中讨论此授权的原因是这些术语经常被混淆,我们需要理解它才能理解 OAuth 2.0 和 OpenID Connect 等概念)

上述两种情况在现实生活中的一个很好的类比就是入住酒店房间。身份验证就像你的护照,而授权就像我是否被允许进入某个房间(因为我预订了)。

OAuth 2.0

OAuth 2.0 是一种授权协议。为了更好地理解它,让我们回顾一下它尚未存在的时代。在下图中,你可以看到一个 Facebook 页面,要求输入 Gmail 密码才能在 Facebook 上搜索联系人并将其添加为好友。

OAuth 之前的 Facebook

OAuth 出现之前的 Facebook 图像,来源:https ://oauth.net/videos/

想想这意味着什么:Facebook 开发人员将可以访问您的 Gmail 密码以及包括电子邮件在内的所有 Gmail 数据。

OAuth 允许 Gmail 等应用仅授予其特定资源的访问权限,在本例中,就是您的联系人。它通过创建一个可以与 API 通信并检索这些数据的访问令牌来实现这一点。

OAuth 为我们提供了 2 个令牌:刷新令牌和访问令牌。

访问令牌是短暂的,使您能够访问受限的 API。

刷新令牌的作用是让我们能够获得新的访问令牌,而不需要用户在每次访问令牌过期时登录,从而带来更好的用户体验。

OpenID 连接 (OIDC)

OpenID是基于 OAuth 2.0 构建的身份验证协议,其主要新增功能是 ID 令牌。ID 令牌旨在用于客户端应用程序,而 OAuth 2.0 提供的访问令牌旨在用于资源服务器(API)。

OAuth 令牌受众

OAuth 令牌受众,来源:https://oauth.net/videos/

智威汤逊

JWT – Json Web Token是双方之间安全表示声明的标准方法。令牌中的信息经过数字签名,以防止篡改。

虽然 OAuth 不强制要求令牌类型,但许多实现使用 JWT 令牌来存储刷新和访问令牌。另一方面,OpenID Connect 规定令牌必须采用
JWT 格式。

NodeJS 微服务身份验证策略

现在我们已经对相关术语有了基本的了解(在我们深入实践之前),我们可以开始探索在微服务中实现身份验证的可能性:

1.显而易见的方法:使用数据库存储用户数据,编写创建用户、注册、存储密码等的逻辑。然后,您可以在客户端创建一个表单,供用户登录,一旦登录,您就可以将用户信息存储在您认为合适的任何地方(cookie、应用程序状态等)。

2. OpenID Connect:您可以使用 Google 和 Facebook 等服务,让用户使用其对应的帐户登录。然后,在数据库中创建相应的用户。这样,您无需实现任何用户创建 UI 或存储密码。

当用户登录时,您可以将 JWT 令牌存储在 Cookie 中,您的微服务可以根据该令牌识别用户。它也可以基于此允许或禁止某些操作,但这超出了本文的讨论范围。

当然,您可以将其与选项一结合起来,让一些用户通过 Google/Facebook 等身份提供商以及您自己的系统中的其他用户创建。

3.使用像Auth0 / Okta这样的身份即服务工具,它们实际上对上述两种用例都有帮助,并且可以节省你自行实现所有功能的时间。我不会深入探讨这一点,但你可以访问他们的网站了解更多信息。

对于本指南的实践部分,我选择了选项 2,因为我觉得它对理解 Node.js 中的身份验证最有帮助,而且您很可能会使用它,无论是直接使用它还是在后台使用它。

实践

让我们开始启用 OpenID 连接来实现我们的服务。

以下是我们要创建的内容:

  1. account-service – 用于处理用户创建的 REST API。我们将使用基于 OpenID Connect 的 Passport 和 Passport-google-oauth 策略。
  2. Greeting-service——一个向用户打招呼的简单 REST API。
  3. 一个 React 应用程序,允许用户通过使用帐户服务使用 Google 登录,并通过问候服务向用户致意。

步骤 1 – 设置 Google 项目以进行登录

1.前往https://console.cloud.google.com/并注册(如果尚未注册)

2.创建新项目

Google OAuth 项目创建
Google OAuth 项目创建

3.选择新创建的项目,点击创建凭证

4.选择 OAuth 客户端 ID

5.选择配置同意屏幕

6.选择“内部”并点击“创建”

7.填写姓名和电子邮件(其余部分可以暂时留空,我们稍后会处理)

8.在范围屏幕中,单击添加范围,然后选择userinfo.email

更新选定的 Google 范围<br>
更新选定的 Google 范围

9.点击“继续”。现在我们进入了同意页面。返回凭证页面,点击“创建凭证”,然后选择“OAuth 客户端 ID”。

10.选择您的名称,对于授权重定向 URI - 添加“ http://localhost:5000/auth/google/callback”并点击创建

11.您现在应该会收到一个包含客户端 ID 和客户端密钥的弹出窗口。请妥善保管,我们很快就会用到它们。

步骤 2 – 创建帐户服务

注意:为了简单起见,本教程将使用 express-generator 生成的默认代码。因此,这里看不到类似 TypeScript 的好看的代码。

1. 使用 express-generator 创建项目并执行所需的安装

npx express-generator --no-view account-service
cd account-service
npm install
npm install --save jsonwebtoken passport passport-google-oauth cors
Enter fullscreen mode Exit fullscreen mode

2. Express 生成器创建了一些默认路线。

我们不会使用用户文件,而是使用 index.js。所以我删除了对它的引用。初始项目如下所示。

快速发电机

3. 在 bin/www 中,我们将端口从 3000 更改为 5000

var port = normalizePort(process.env.PORT || '5000');
Enter fullscreen mode Exit fullscreen mode

4. 添加护照

Passport 是 Node.js 的身份验证中间件。它使用起来非常简单,并且支持我们需要的所有选项,所以我选择使用它。Passport 使用一些策略来处理某些类型的登录。passport-google-login 是一个基于 OpenID Connect 的 Google 登录策略。

在我们的 app.js 文件中 - 让我们添加以下代码,使其看起来像这样:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require('cors');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

const indexRouter = require('./routes/index');

const app = express();
// This is here for our client side to be able to talk to our server side. you may want to be less permissive in production and define specific domains.
app.use(cors());

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));


app.use(passport.initialize());
app.use(passport.session());

app.use('/', indexRouter);

passport.serializeUser(function(user, cb) {
 cb(null, user);
});

passport.deserializeUser(function(obj, cb) {
 cb(null, obj);
});

passport.use(new GoogleStrategy({
   clientID: 'your-google-client-id',
   clientSecret: 'your-google-client-secret',
   callbackURL: "http://localhost:5000/auth/google/callback"
 },
 function(accessToken, refreshToken, profile, done) {
   // here you can create a user in the database if you want to
   return done(null, profile);
 }
));

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

请注意,这里有一些有趣的事情。

一、我们为客户端 (localhost:3000) 添加了 cors,以便能够从服务器端发出请求。出于安全性考虑,您可能希望在生产环境中仅允许特定域名访问。

序列化和反序列化用户——这些函数负责将用户序列化到会话中以及从会话中反序列化。

GoogleStrategy – 这就是我们告诉 Passport 我们将使用谷歌身份验证的方式。

还记得之前保存的客户端 ID 和密钥吗?现在就可以插入它们了。

5.添加身份验证路由

现在让我们转到 routes/index.js 文件并添加相关路线。

const express = require('express');
const router = express.Router();
const passport = require('passport');
const jwt = require('jsonwebtoken');

router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express' });
});

const TOKEN_SECRET = 'SECRET';

router.get('/auth/google',
 passport.authenticate('google', { scope : ['profile', 'email'] }));

router.get('/auth/google/callback',
 passport.authenticate('google', { failureRedirect: '/error' }),
 function(req, res) {
   const token = jwt.sign({ id: req.user.sub, name: req.user.name }, TOKEN_SECRET, {
     expiresIn: 60 * 60,
   });
   res.cookie('auth', token, { httpOnly: true });
   res.redirect('http://localhost:3000/');
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

TOKEN_SECRET – 我们将使用它来签署我们的 JWT 令牌。

/auth/google – 实际的 google 登录路线。

用户会被重定向到 Google。完成后,他们会被重定向回我们的服务器 /auth/google/callback。在那里我们可以创建 JWT 令牌。

创建后,我们会将其作为 Cookie 添加到请求中httpOnly,这样 JavaScript 代码就无法访问它(这在安全性方面是很好的)。您很快就会看到它在客户端是如何工作的。

准备就绪后,我们将重定向回客户端。

附注:为了演示目的,我们将名称存储在 JWT 中,但您可能不需要它,并且从安全性方面来看可能不是最佳做法。

现在我们已经完成了帐户服务,让我们继续客户端。

步骤 3:创建客户端 React 应用程序

npx create-react-app auth-strategies-client
cd auth-strategies-client/
yarn add axios
Enter fullscreen mode Exit fullscreen mode

我们现在有了一个默认的 React 应用。让我们修改应用的 js 文件,使其包含一个指向 Google 身份验证的链接。

import logo from './logo.svg';
import './App.css';

function App() {
 return (
   <div className="App">
     <header className="App-header">
       <img src={logo} className="App-logo" alt="logo" />
       <a href="http://localhost:5000/auth/google">Sign in with Google</a>
     </header>
   </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

使用 yarn start 运行后,它看起来如下:
使用 Google 登录

点击“使用 Google 登录”。完成后,您将被重定向到 Google 进行身份验证。

(确保在 accounts-service 上运行 npm start 以使其在端口 5000 上运行)。

让我们看看调用 accounts-service/auth/google/callback 之后发生了什么:

1.accounts-service 向 google 发出 POST 请求,返回访问令牌和 id 令牌。

Aspecto 实时流查看器

放大查看响应
放大查看响应

放大服务
放大服务

[附言:这些图像是使用 Aspecto 的实时流量查看器生成的。如果您想像我一样将服务可视化,可以尝试 Aspecto(免费)。开始发送流量需要 2 分钟]。

2.它使用该令牌向 Google 发出另一个 GET 请求以获取用户的个人信息。

向 Google 发出另一个 GET 请求

向 Google 发出另一个 GET 请求,放大查看响应
放大查看响应

3.我们的微服务已将我们重定向回客户端,并在响应中使用 set-cookie 来创建我们的身份验证 cookie。

我们的微服务已将我们重定向回客户端

因此 - 之后,您将被重定向回完全相同的屏幕,但有一点区别:如果您打开浏览器的 DevTools,您应该会看到一个仅限 http 的身份验证 cookie:

浏览器的 DevTools

现在我们可以使用这个令牌了。这将引导我们进入我们的greeting-service。请保留客户端,因为我们很快就会修改它。

步骤 4 – 设置greetings服务

1. 创建我们的服务

npx express-generator --no-view greetings-service
cd greetings-service
npm install
npm install --save passport passport-jwt cors
Enter fullscreen mode Exit fullscreen mode

2.删除 user.js 文件和 app.js 中的路由,这是我们的新开始:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');

const indexRouter = require('./routes/index');

const app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

3.修改bin/www中的端口为5001

var port = normalizePort(process.env.PORT || '5001');
Enter fullscreen mode Exit fullscreen mode

4.通过修改app.js设置护照来读取我们的JWT令牌:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require('cors');

const indexRouter = require('./routes/index');

const app = express();
app.use(cors({ credentials: true, origin: 'http://localhost:3000' }));


app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy,
 ExtractJwt = require('passport-jwt').ExtractJwt;

app.use(passport.initialize());
app.use(passport.session());

app.use('/', indexRouter);

const cookieExtractor = function(req) {
 let token = null;
 if (req && req.cookies)
 {
   token = req.cookies['auth'];
 }
 return token;
};

const TOKEN_SECRET = 'SECRET';

const opts = {
 jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor]),
 secretOrKey: TOKEN_SECRET,
};

passport.use(
 'jwt',
 new JwtStrategy(opts, (jwt_payload, done) => {
   try {
     console.log('jwt_payload', jwt_payload);
     done(null, jwt_payload);
   } catch (err) {
     done(err);
   }
 }),
);

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

这里我们需要注意一些有趣的事情。

cookieExtractor负责从httpOnly我们之前创建的 cookie 中读取令牌,并将其与请求一起传递(稍后会详细介绍)。

请注意,我们必须使用与创建令牌相同的 TOKEN_SECRET 来读取它,否则读取时会收到无效签名错误。

然后,提取器被传递给JwtStrategy,它负责为我们提供jwt_payload。如果我们要添加数据库,我们可以从数据库获取更多关于用户的信息,但为了简单起见,我决定不这样做。

现在我们将在 index.js 中添加问候路线:

const express = require('express');
const router = express.Router();
const passport = require('passport');

router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express' });
});

router.get('/greetme', (req, res, next) => {
 passport.authenticate('jwt', { session: false }, (err, user, info) => {
   if (err) {
     console.log('error is', err);
     res.status(500).send('An error has occurred, we cannot greet you at the moment.');
   }
   else {
     res.send({ success: true, fullName: `${user.name.givenName} ${user.name.familyName}` })
   }
 })(req, res, next);
});


module.exports = router;
Enter fullscreen mode Exit fullscreen mode

这里发生的情况是,Passport 为我们从 JWT 中提取信息,我们所做的就是将其返回给客户端。

在端口 5001 启动greetings-service:

npm start
Enter fullscreen mode Exit fullscreen mode

步骤 5 – 修改客户端以发送 httpOnly cookie

由于我们希望客户端 JWT 令牌不被任何恶意 javascript 代码访问,因此我们将其存储在 httpOnly cookie 中。

(附注:在现实生活中,您可能还希望确保其安全,以便只能通过 HTTPS 访问)。

所以,我们要向greetings-service发送问候请求。为此,我们需要将cookie的内容发送到服务器。那就开始吧。

回到我们的客户端 React 应用程序,我们通过添加一个按钮来修改 App.js:

import React, { useState } from 'react';
import axios from "axios";
import logo from './logo.svg';
import './App.css';

function App() {
 const [name, setName] = useState('');
 return (
   <div className="App">
     <header className="App-header">
       <img src={logo} className="App-logo" alt="logo" />
       <a href="http://localhost:5000/auth/google">Sign in with Google</a>
       <br />
       <button onClick={async () => {
         const result = await axios.get('http://localhost:5001/greetme', {
           withCredentials: true
         });
         setName(result.data.fullName);
       }}>Greet me please</button>
       {name && <span>{`Hi, ${name}`}</span>}
     </header>
   </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

现在,一旦我们收到包含当前用户全名的回复,我们就会看到“嗨,全名”。

请注意,我们添加了一个带有 :true 的基本 axios withCredentials- 这使得 cookie 与我们的请求一起传递,以供服务器提取。

这强调了幕后发生的事情:

Aspecto 实时流查看器

一个简单的 GET 请求,返回一个包含用户全名的 JSON,因为它来自 google 并存储在 JWT 令牌中。

以下是我们点击按钮后得到的结果:

反应汤姆·扎克

就是这样!

我们已经成功创建了用于注册和 JWT 创建的帐户服务,以及知道如何读取 JWT 令牌并提供有关用户数据的问候服务。

我希望这能帮助您更好地理解身份验证,特别是在 nodejs 中实现身份验证。

查看我的其他一些文章,例如这篇:Lerna Hello World:如何为多个节点包创建 Monorepo

鏂囩珷鏉ユ簮锛�https://dev.to/aspecto/microservices-authentication-strategies-theory-to-practice-86i
PREV
每日编程谜题 - 10 月 29 日至 11 月 2 日
NEXT
不使用 Provider + useMutableSource 的 React Context