微服务身份验证策略:理论到实践
理论
实践
在本文中,我们将介绍实现身份验证微服务的常见方法。
我们将分为两部分:
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 图像,来源: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 令牌受众,来源: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 连接来实现我们的服务。
以下是我们要创建的内容:
- account-service – 用于处理用户创建的 REST API。我们将使用基于 OpenID Connect 的 Passport 和 Passport-google-oauth 策略。
- Greeting-service——一个向用户打招呼的简单 REST API。
- 一个 React 应用程序,允许用户通过使用帐户服务使用 Google 登录,并通过问候服务向用户致意。
步骤 1 – 设置 Google 项目以进行登录
1.前往https://console.cloud.google.com/并注册(如果尚未注册)
2.创建新项目
3.选择新创建的项目,点击创建凭证
4.选择 OAuth 客户端 ID
5.选择配置同意屏幕
6.选择“内部”并点击“创建”
7.填写姓名和电子邮件(其余部分可以暂时留空,我们稍后会处理)
8.在范围屏幕中,单击添加范围,然后选择userinfo.email
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
2. Express 生成器创建了一些默认路线。
我们不会使用用户文件,而是使用 index.js。所以我删除了对它的引用。初始项目如下所示。
3. 在 bin/www 中,我们将端口从 3000 更改为 5000
var port = normalizePort(process.env.PORT || '5000');
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;
请注意,这里有一些有趣的事情。
一、我们为客户端 (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;
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
我们现在有了一个默认的 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;
点击“使用 Google 登录”。完成后,您将被重定向到 Google 进行身份验证。
(确保在 accounts-service 上运行 npm start 以使其在端口 5000 上运行)。
让我们看看调用 accounts-service/auth/google/callback 之后发生了什么:
1.accounts-service 向 google 发出 POST 请求,返回访问令牌和 id 令牌。
[附言:这些图像是使用 Aspecto 的实时流量查看器生成的。如果您想像我一样将服务可视化,可以尝试 Aspecto(免费)。开始发送流量需要 2 分钟]。
2.它使用该令牌向 Google 发出另一个 GET 请求以获取用户的个人信息。
3.我们的微服务已将我们重定向回客户端,并在响应中使用 set-cookie 来创建我们的身份验证 cookie。
因此 - 之后,您将被重定向回完全相同的屏幕,但有一点区别:如果您打开浏览器的 DevTools,您应该会看到一个仅限 http 的身份验证 cookie:
现在我们可以使用这个令牌了。这将引导我们进入我们的greeting-service。请保留客户端,因为我们很快就会修改它。
步骤 4 – 设置greetings服务
1. 创建我们的服务
npx express-generator --no-view greetings-service
cd greetings-service
npm install
npm install --save passport passport-jwt cors
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;
3.修改bin/www中的端口为5001
var port = normalizePort(process.env.PORT || '5001');
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;
这里我们需要注意一些有趣的事情。
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;
这里发生的情况是,Passport 为我们从 JWT 中提取信息,我们所做的就是将其返回给客户端。
在端口 5001 启动greetings-service:
npm start
步骤 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;
现在,一旦我们收到包含当前用户全名的回复,我们就会看到“嗨,全名”。
请注意,我们添加了一个带有 :true 的基本 axios withCredentials
- 这使得 cookie 与我们的请求一起传递,以供服务器提取。
这强调了幕后发生的事情:
一个简单的 GET 请求,返回一个包含用户全名的 JSON,因为它来自 google 并存储在 JWT 令牌中。
以下是我们点击按钮后得到的结果:
就是这样!
我们已经成功创建了用于注册和 JWT 创建的帐户服务,以及知道如何读取 JWT 令牌并提供有关用户数据的问候服务。
我希望这能帮助您更好地理解身份验证,特别是在 nodejs 中实现身份验证。
查看我的其他一些文章,例如这篇:Lerna Hello World:如何为多个节点包创建 Monorepo。
鏂囩珷鏉ユ簮锛�https://dev.to/aspecto/microservices-authentication-strategies-theory-to-practice-86i