本文是在 Angular 应用中设计和实现基于 JWT(JSON Web Tokens)身份验证的分步指南。前端
咱们的目标是系统的讨论基于 JWT 的认证设计和实现,衡量取舍不一样的设计方案,并将其应用到某个 Angular 应用特定的上下文中。node
咱们将追踪一个 JWT 从被认证服务器建立开始,到它被返回到客户端,再到它被返回到应用服务器的全程,并讨论其中涉及的全部的方案以及作出的决策。android
因为身份验证一样须要一些服务端代码,因此咱们将同时显示这些信息,以便咱们能够掌握整个上下文,而且看清楚各个部分之间如何协做。ios
服务端代码是 Node/Typescript,Angular 开发者对这些应该是很是熟悉的。可是涵盖的概念并非特定于 Node 的。git
若是你使用另外一种服务平台,主须要在 jwt.io 上为你的平台选择一个 JWT 库,这些概念仍然适用。github
在这篇文章中,咱们将介绍如下主题:web
因此无需再费周折(without further ado),咱们开始学习基于 JWT 的 Angular 的认证吧!express
首先介绍如何使用 JSON 网络令牌来创建用户会话:简而言之,JWT 是数字签名以 URL 友好的字符串格式编码的 JSON 有效载荷(payload)。json
JWT 一般能够包含任何有效载荷,但最多见的用例是使用有效载荷来定义用户会话。后端
JWT 的关键在于,咱们只须要检查令牌自己验证签名就能够肯定它们是否有效,而无需为此单独联系服务器,不须要将令牌保存到内存中,也不须要在请求的时候保存到服务器或内存中。
若是使用 JWT 身份验证,则它们将至少包含用户 ID 和过时时间戳。
若是你想要深刻了解有关 JWT 格式的详细信息(包括最经常使用的签名类型如何工做),请参阅本文后面的 JWT: The Complete Guide to JSON Web Tokens 一文。
若是想知道 JWT 是什么样子的话,下面是一个例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNTM0NTQzNTQzNTQzNTM0NTMiLCJleHAiOjE1MDQ2OTkyNTZ9.zG-2FvGegujxoLWwIQfNB5IT46D-xC4e8dEDYwi6aRM
复制代码
你可能会想:这看起来不像 JSON!那么 JSON 在哪里?
为了看到它,让咱们回到 jwt.io 并将完成的 JWT 字符串粘贴到验证工具中,而后咱们就能看到 JSON 的有效内容:
{
"sub": "353454354354353453",
"exp": 1504699256
}
复制代码
sub
属性包含用户标识符,exp
包含用户过时时间戳.这种类型的令牌被称为不记名令牌(Bearer Token),意思是它标识拥有它的用户,并定义一个用户会话。
不记名令牌是用户名/密码组合的签名临时替换!
若是想进一步了解 JWT,请看看这里。对于本文的其他部分,咱们将假定 JWT 是一个包含可验证 JSON 有效载荷的字符串,它定义了一个用户会话。
实现基于 JWT 的身份验证第一步是发布不记名令牌并将其提供给用户,这是登陆/注册页面的主要目的。
身份验证以登录页面开始,该页面能够托管在咱们的域中或者第三方域中。在企业场景中,登录页面通常会托管在单独的服务器上。这是公司范围内单点登陆解决方案的一部分。
在公网(Public Internet)上,登陆页面也多是:
单独托管的登陆页面是一种安全性的改进,由于这样密码永远不会直接由咱们的应用代码来处理。
单独托管的登陆页面能够具备最少许的 JavaScript 甚至彻底没有,而且能够将其作到不论看起来仍是用起来都像是总体应用的一部分的效果。
可是,用户在咱们应用中经过内置登陆页面登陆也是一种可行且经常使用的解决方案,因此咱们也会介绍一下。
若是直接在咱们的 SPA 程序中建立登陆页面,它将看起来是这样的:
@Component({
selector: 'login',
template: `
<form [formGroup]="form">
<fieldset>
<legend>Login</legend>
<div class="form-field">
<label>Email:</label>
<input name="email" formControlName="email">
</div>
<div class="form-field">
<label>Password:</label>
<input name="password" formControlName="password"
type="password">
</div>
</fieldset>
<div class="form-buttons">
<button class="button button-primary"
(click)="login()">Login</button>
</div>
</form>`})
export class LoginComponent {
form:FormGroup;
constructor(private fb:FormBuilder,
private authService: AuthService,
private router: Router) {
this.form = this.fb.group({
email: ['',Validators.required],
password: ['',Validators.required]
});
}
login() {
const val = this.form.value;
if (val.email && val.password) {
this.authService.login(val.email, val.password)
.subscribe(
() => {
console.log("User is logged in");
this.router.navigateByUrl('/');
}
);
}
}
}
复制代码
正如咱们所看到的,这个页面是一个简单的表单,包含两个字段:电子邮件和密码。当用户点击登陆按钮的时候,用户和密码将经过 login()
调用发送到客户端身份验证服务。
把咱们全部的客户端身份验证逻辑放在一个集中的应用范围内的单个 AuthService
(认证服务)中将帮助咱们保持咱们代码的组织结构。
这样,若是之后咱们须要更改安全提供者或者重构咱们的安全逻辑,咱们只须要改变这个类。
在这个服务里,咱们将使用一些 JavaScript API 来调用第三方服务,或者使用 Angular HTTP Client 进行 HTTP POST 调用。
这两种方案的目标是一致的:经过 POST 请求将用户和密码组合经过网络传送到认证服务器,以便验证密码并启动会话。
如下是咱们如何使用 Angular HTTP Client 构建本身的 HTTP POST:
@Injectable()
export class AuthService {
constructor(private http: HttpClient) {
}
login(email:string, password:string ) {
return this.http.post<User>('/api/login', {email, password})
// 这只是一个 HTTP 调用,
// 咱们还须要去处理 token 的接收
.shareReplay();
}
}
复制代码
咱们调用的 shareReplay
能够防止这个 Observable 的接收者因为屡次订阅而意外触发多个 POST 请求。
在处理登陆响应以前,咱们先来看看请求的流程,看看服务器上发生了什么。
不管咱们在应用级别使用登陆页面仍是托管登陆页面,处理登陆 POST 请求的服务器逻辑是相同的。
目标是在这两种状况下都会验证密码并创建一个会话。若是密码是正确的,那么服务器将会发出一个不记名令牌,说:
该令牌的持有者的专业 ID 是 353454354354353453, 该会话在接下来的两个小时有效
而后服务器应该对令牌进行签名并发送回用户浏览器!关键部分是 JWT 签名:这是防止攻击者伪造会话令牌的惟一方式。
这是使用 Express 和 Node 包 node-jsonwebtoken 建立新的 JWT 会话令牌的代码:
import {Request, Response} from "express";
import * as express from 'express';
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
import * as jwt from 'jsonwebtoken';
import * as fs from "fs";
const app: Application = express();
app.use(bodyParser.json());
app.route('/api/login')
.post(loginRoute);
const RSA_PRIVATE_KEY = fs.readFileSync('./demos/private.key');
export function loginRoute(req: Request, res: Response) {
const email = req.body.email,
password = req.body.password;
if (validateEmailAndPassword()) {
const userId = findUserIdForEmail(email);
const jwtBearerToken = jwt.sign({}, RSA_PRIVATE_KEY, {
algorithm: 'RS256',
expiresIn: 120,
subject: userId
}
// 将 JWT 发回给用户
// TODO - 多种可选方案
}
else {
// 发送状态 401 Unauthorized(未经受权)
res.sendStatus(401);
}
}
复制代码
代码不少,咱们逐行分解:
bodyParser.json()
中间件,使 Express 可以从 HTTP 请求体中读取 JSON 有效载荷loginRoute
的路由处理程序,若是服务器收到一个目标地址是 /api/login
的 POST 请求,就会触发它在 loginRoute
方法中,咱们有一些代码展现了如何实现登陆路由:
bodyParser.json()
中间件的存在,咱们可使用 req.body
访问 JSON 请求主体有效载荷。.sign()
调用结果是 JWT 字符串自己总而言之,咱们验证了密码并建立一个 JWT 会话令牌。如今咱们已经对这个代码的工做原理有了一个很好的了解,让咱们来关注使用了 RS256 签名的包含用户会话详细信息的 JWT 签名的关键部分。
为何签名的类型很重要?由于没有理解它,咱们就没法理解应用程序服务端上对相关令牌的验证代码。
RS256 是基于 RSA 的 JWT 签名类型,是一种普遍使用的公钥加密技术。
使用 RS256 签名的主要优势之一是咱们能够将建立令牌的能力与验证他们的能力分开。
若是您想知道如何手动重现它们,能够阅读 JWT 指南中使用此类签名的全部优势。
简而言之,RS256 签名的工做方式以下:
RSA_PRIVATE_KEY
)用于对 JWT 进行签名为何使用公钥加密签署 JWT ?如下是一些安全和运营优点的例子:
最后一部分是一个很好的特性:可以发布验证密钥给咱们内置的密钥轮换或者撤销,咱们将在这篇文章中实现!
这是由于(使用 RS256)为了启用一个新的密钥对,咱们只须要发布一个新的公钥,而且咱们会看到这个公钥。
另外一个经常使用的签名是 HS256,没有这些优点。
HS256 仍然是经常使用的,可是例如 Auth0 等供应商如今都默认使用 RS256。若是你想了解有关 HS256,RS256 和 JWT 签名的更多信息,请查看这篇文章
抛开咱们使用的签名类型不谈,咱们须要将新签名的令牌发送回用户浏览器。
咱们有几种不一样的方式将令牌发回给用户,例如:
让咱们从 cookie 开始,为何不使用 Cookie 呢?JWT 有时候被称为 Cookie 的替代品,但这是两个彻底不一样的概念。 Cookie 是一种浏览器数据存储机制,能够安全地存储少许数据。
该数据能够是诸如用户首选语言之类的任何数据。但它也能够包含诸如 JWT 的用户识别令牌。
所以,咱们能够将 JWT 存储在 Cookie 中!而后,咱们来谈谈使用 Cookie 存储 JWT 与其余方法相比较的优势和缺点。
Cookie 的一个独特之处在于,浏览器会自动为每一个请求附加到特定域和子域的 Cookie 到 HTTP 请求的头部。
这就意味着,若是咱们将 JWT 存储到了 Cookie 中,假设登陆页面和应用共享一个根域,那么在客户端上,咱们不须要任何其余的的逻辑,就可让 Cookie 随每个请求发送到应用服务器。
而后,让咱们把 JWT 存储到 Cookie 中,看看会发生什么。下面是咱们对登陆路由的实现,发送 JWT 到浏览器,存入 :
... continuing the implementation of the Express login route
// this is the session token we created above
const jwtBearerToken = jwt.sign(...);
// set it in an HTTP Only + Secure Cookie
res.cookie("SESSIONID", jwtBearerToken, {httpOnly:true, secure:true});
复制代码
除了使用 JWT 值设置 Cookie 外,咱们还设置了一些咱们将要讨论的安全属性。
Cookie 另外一个独特之处在于它有着一些与安全相关的属性,有助于确保数据的安全传输。
一个 Cookie 能够标记为“安全”,这意味着若是浏览器经过 HTTPS 链接发起了请求,那么它只会附加到请求中。
一个 Cookie 一样能够被标记为 Http Only,这就意味着它 根本不能 被 JavaScript 代码访问!请注意,浏览器依旧会将 Cookie 附加到对服务器的每一个请求中,就像使用其余 Cookie 同样。
这意味着,当咱们删除 HTTP Only 的 Cookie 的时候,咱们须要向服务器发送请求,例如注销用户。
HTTP Only 的 Cookie 的一个优势是,若是应用遭受脚本注入攻击(或称 XSS),在这种荒谬的状况下, Http Only 标志仍然会阻止攻击者访问 Cookie ,阻止使用它冒充用户。
Secure 和 Http Only 标志常常能够一块儿使用,以得到最大的安全性,这可能使咱们认为 Cookie 是存储 JWT 的理想场所。
可是 Cookie 也有一些缺点,那么咱们来谈谈这些:这将有助于咱们知晓在 JWT 中存储 Cookie 是不是一种适合咱们应用的好方案。(译者注:原文是 “this will help us decide if storing cookies in a JWT is a good approach for our application”,可是上面的部分讲的是将 JWT 存入 Cookie 中,因此译者认为原文有误,可是仍是选择尊重原文)
将不记名令牌存储在 Cookie 中的应用,所以(由于这个 Cookie)遭受的攻击被称为跨站请求伪造(Cross-Site Request Forgery),也成为 XSRF 或者 CSRF。下面是其原理:
这就意味着攻击者能够欺骗用户表明他去执行某些操做,只须要发送一封电子邮件或者公共论坛上发布连接便可。
这个攻击不像看起来那么吓人,但问题是执行起来很简单:只须要一封电子邮件或者社交媒体上的帖子。
咱们会在后文详细介绍这种攻击,如今须要知道的是,若是咱们选择将咱们的 JWT 存储到 Cookie 中,那么咱们还须要对 XSRF 进行一些防护。
好消息是,全部的主流框架都带有防护措施,能够很容易地对抗 XSRF,由于它是一个众所周知的漏洞。
就像是发生过不少次同样,Cookie 设计上鱼和熊掌不能兼得:使用 Cookie 意味着利用 HTTP Only 能够很好的防护脚本注入,可是另外一方面,它引入了一个新的问题 —— XSRF。
在 Cookie 中接收会话 JWT 的潜在问题是,咱们没法从处理验证逻辑的第三方域接收到它。
这是由于在 app.example.com
运行的应用不能从 security-provider.com
等其余域访问 Cookie。 所以在这种状况下,咱们将没法访问包含 JWT 的 Cookie,并将其发送到咱们的服务器进行验证,这个问题致使了 Cookie 不可用。
第三方认证提供商可能会容许咱们在咱们本身网站的可配置子域名中运行外部托管的登陆页面,例如 login.example.com
。
所以,将全部这些解决方案中最好的部分组合起来是有可能的。下面是解决方案的样子:
login.example.com
上,example.com
上运行应用这将为咱们提供最大限度的保护,防止密码和身份令牌被盗:
这种状况有时用于企业门户,能够提供很好的安全功能。可是这须要咱们的登陆页面支持托管到自定义域,且使用了安全提供程序或企业安全代理。
可是,此功能(登陆页面托管到自定义子域)并不老是可用,这使得 HTTP Only Cookie 方法可能失效。
若是你的应用属于这种状况,或者你正寻找不依赖 Cookie 的替代方案,那么让咱们回到最初的起点,看看咱们能够作什么。
具备 HTTP Only 特性的 Cookie 是存储 JWT 的可靠选择,可是还会有其余很好的选择。例如咱们不使用 Cookie,而是在 HTTP 响应体中将 JWT 发送回客户端。
咱们不只要发送 JWT 自己,并且还要将过时时间戳做为单独的属性发送。
的确,过时时间戳在 JWT 中也能够获取到,可是咱们但愿让客户端可以简单地得到会话持续时间,而没必要要为此再安装一个 JWT 库。
如下使咱们如何在 HTTP 响应体中将 JWT 发送回客户端:
... 继续 Express 登陆路由的实现
// 这是咱们上面建立的会话令牌
const jwtBearerToken = jwt.sign(...);
// 将其放入 HTTP 响应体中
res.status(200).json({
idToken: jwtBearerToken,
expiresIn: ...
});
复制代码
这样,客户端将收到 JWT 及其过时时间戳。
不使用 Cookie 的优势是咱们的应用再也不容易受到 XSRF 攻击,这是这种方法的优势之一。
可是这一样意味着咱们将不得不添加一些客户端代码来处理令牌,由于浏览器将再也不为每一个向应用服务器发送的请求转发它。
这也意味着,在成功的脚本注入攻击的状况下,攻击者此时能够读取到 JWT 令牌,而存储到 HTTP Only Cookie 则不可能读取到。
这是与选择安全解决方案有关的设计折衷的一个好例子:一般是安全与便利的权衡。
让咱们继续跟随咱们的 JWT 不记名令牌的旅程。因为咱们将 JWT 经过请求体发回给客户端,咱们须要阅读并处理它。(译者注:原文是“Since we are sending the JWT back to the client in the request body”,译者认为应该是响应体(response body),可是尊重原文)
一旦咱们在客户端收到了 JWT,咱们须要把它存储在某个地方。不然,若是咱们刷新浏览器,它将会丢失。那么咱们就必需要从新登陆了。
有不少地方能够保存 JWT(Cookie 除外)。本地存储(Local Storage)是存储 JWT 的实用场所,它是以字符串的键值对的形式存储的,很是适合存储少许数据。
请注意,本地存储具备同步 API。让咱们来看看实用本地存储的登陆与注销逻辑的实现:
import * as moment from "moment";
@Injectable()
export class AuthService {
constructor(private http: HttpClient) {
}
login(email:string, password:string ) {
return this.http.post<User>('/api/login', {email, password})
.do(res => this.setSession)
.shareReplay();
}
private setSession(authResult) {
const expiresAt = moment().add(authResult.expiresIn,'second');
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem("expires_at", JSON.stringify(expiresAt.valueOf()) );
}
logout() {
localStorage.removeItem("id_token");
localStorage.removeItem("expires_at");
}
public isLoggedIn() {
return moment().isBefore(this.getExpiration());
}
isLoggedOut() {
return !this.isLoggedIn();
}
getExpiration() {
const expiration = localStorage.getItem("expires_at");
const expiresAt = JSON.parse(expiration);
return moment(expiresAt);
}
}
复制代码
让咱们分析一下这个实现过程当中发生了什么,从 login 方法开始:
expiresIn
属性的 login 调用的结果,并直接将它传递给 setSession
方法setSession
中,咱们直接将 JWT 存储到本地存储中的 id_token
键值中expiresIn
属性计算过时时间戳expires_at
条目中的一个数字值如今咱们在客户端拥有所有的会话信息,咱们能够在客户端应用的其他部分使用这些信息。
例如,客户端应用须要知道用户是否登录或者注销,以判断某些好比登陆/注销菜单按钮这类的 UI 元素的显示与否。
这些信息如今能够经过 isLoggedIn()
, isLoggedOut()
和 getExpiration()
获取。
如今咱们已经将 JWT 保存在用户浏览器中,让咱们继续追随其在网络中的旅程。
让咱们来看看如何使用它来让应用服务器知道一个给定的 HTTP 请求属于特定用户。这是认证方案的所有要点。
如下是咱们须要作的事情:咱们须要用某种方式为 HTTP 附加 JWT,并发送到应用服务器。
而后应用服务器将验证请求并将其连接到用户,只须要检查 JWT,检查其签名并从有效内容中读取用户标识。
为了确保每一个请求都包含一个 JWT,咱们将使用一个 Angular HTTP 拦截器。
如下是 Angular 拦截器的代码,用于为每一个请求附加 JWT 并发送给应用服务器:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>,
next: HttpHandler): Observable<HttpEvent<any>> {
const idToken = localStorage.getItem("id_token");
if (idToken) {
const cloned = req.clone({
headers: req.headers.set("Authorization",
"Bearer " + idToken)
});
return next.handle(cloned);
}
else {
return next.handle(req);
}
}
}
复制代码
那么让咱们来分解如下这个代码是如何工做:
而且在此处,最初在认证服务器上建立的 JWT 如今会随着每一个请求发送到应用服务器。
咱们来看看应用服务器如何使用 JWT 来识别用户。
为了验证请求,咱们须要从 Authorization
头中提取 JWT,并检查时间戳和用户标识符。
咱们不但愿将这个逻辑应用到全部的后端路由,由于某些路由是全部用户公开访问的。例如,若是咱们创建了本身的登录和注册路由,那么这些路由应该能够被全部用户访问。
另外,咱们不但愿在每一个路由基础上都重复验证逻辑,所以最好的解决方案是建立一个 Express 认证中间件,并将其应用于特定的路由。
假设咱们已经定义了一个名为 checkIfAuthenticated
的 express 中间件,这是一个可重用的函数,它只在一个地方包含认证逻辑。
如下是咱们如何将其应用于特定的路由:
import * as express from 'express';
const app: Application = express();
// ... 定义 checkIfAuthenticated 中间件
// 检查用户是否仅在某些路由进行身份验证
app.route('/api/lessons')
.get(checkIfAuthenticated, readAllLessons);
复制代码
在这个例子中,readAllLessons
是一个 Express 路由,若是一个 GET 请求到达 /api/lessons
Url,它就会提供一个 JSON 列表。
咱们已经经过在 REST 端点以前应用 checkIfAuthenticated
中间件,使得这个路由只能被认证的用户访问,这意味着中间件功能的顺序很重要。
若是没有有效的 JWT,checkIfAuthenticated
中间件将会报错,或容许请求经过中间件链继续。
在 JWT 存在的状况下,若是签名正确可是过时,中间件也须要抛出错误。请注意,在使用基于 JWT 的身份验证的任何应用中,全部这些逻辑都是相同的。
咱们可使用 node-jsonwebtoken 本身编写的中间件,可是这个逻辑很容易出错,因此咱们使用第三方库。
为了建立 checkIfAuthenticated
中间件,咱们将使用 express-jwt 库。
这个库可让咱们快速建立经常使用的基于 JWT 的身份验证设置的中间件,因此咱们来看看如何使用它来验证 JWT,好比咱们在登陆服务中建立 JWT(使用 RS256 签名)。
首先假定咱们首先在服务器的文件系统中安装了签名验证公钥。如下是咱们如何使用它来验证 JWT:
const expressJwt = require('express-jwt');
const RSA_PUBLIC_KEY = fs.readFileSync('./demos/public.key');
const checkIfAuthenticated = expressJwt({
secret: RSA_PUBLIC_KEY
});
app.route('/api/lessons')
.get(checkIfAuthenticated, readAllLessons);
复制代码
如今让咱们逐行分解代码:
express-jwt
,而且咱们获得一个准备使用的中间件函数!若是认证头没有正确签名的 JWT,那么这个中间件将会抛出错误。若是 JWT 签名正确,可是已通过期,中间件也会抛出错误。
若是咱们想要改变默认的异常处理方法,好比不将异常抛出。而是返回一个状态码 401 和一个 JSON 负载的消息,这也是能够的。
使用 RS256 签名的主要优势之一是咱们不须要像咱们在这个例子中所作的那样,在应用服务器上安装公钥。
想象一下,服务器上有几个正在运行的实例:在任何地方同时替换公钥都会出现问题。
由认证服务器在公开访问的 URL 中发布用于验证 JWT 的公钥。而不是在应用服务器上安装公钥。
这给咱们带来了不少好处,好比说能够简化密钥轮换和撤销。若是咱们须要一个新的密钥对,咱们只须要发布一个新的公钥。
一般密钥周期轮换期间内,咱们会将两个密钥发布和激活一段时间,这段时间通常大于会话时序时间,目的是不中断用户体验,然而撤销可能会更有效。
攻击者可使用公钥,可是这没有危险。攻击者可使用公钥进行攻击的惟一方法是验证现有 JWT 签名,但是这对攻击者无用。
攻击者没法使用公钥伪造新建立的 JWT,或者以某种方式使用公钥猜想私钥签名值。(译者注:这一部分主要涉及的是对称加密和非对称加密,感受说的很啰嗦)
如今的问题是,如何发布公钥?
JWKS 或者 JSON Web 密钥集 是用于在 REST 端点中基于 JSON 标准发布的公钥。
这种类型的端点输出有点吓人,但好消息是咱们没必要直接使用这种格式,由于有一个库直接使用了它:
{
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"x5c": [
"MIIDJTCCAg2gAwIBAgIJUP6A\/iwWqvedMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWFuZ3VsYXJ1bml2LXNlY3VyaXR5LWNvdXJzZS5hdXRoMC5jb20wHhcNMTcwODI1MTMxNjUzWhcNMzEwNTA0MTMxNjUzWjAwMS4wLAYDVQQDEyVhbmd1bGFydW5pdi1zZWN1cml0eS1jb3Vyc2UuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUvZ+4dkT2nTfCDIwyH9K0tH4qYMGcW\/KDYeh+TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh\/TQ\/8M\/aJ\/Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA+usFAfixPnU5L5lyacj3t+dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ\/\/TKkGadZxBo8FObdKuy7XrrOvug4FAKe+3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N+KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH\/MB0GA1UdDgQWBBRwgr0c0DYG5+GlZmPRFkg3+xMWizAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBACBV4AyYA3bTiYWZvLtYpJuikwArPFD0J5wtAh1zxIVl+XQlR+S3dfcBn+90J8A677lSu0t7Q7qsZdcsrj28BKh5QF1dAUQgZiGfV3Dfe4\/P5wUaaUo5Y1wKgFiusqg\/mQ+kM3D8XL\/Wlpt3p804dbFnmnGRKAJnijsvM56YFSTVO0JhrKv7XeueyX9LpifAVUJh9zFsiYMSYCgBe3NIhIfi4RkpzEwvFIBwtDe2k9gwIrPFJpovZte5uvi1BQAAoVxMuv7yfMmH6D5DVrAkMBsTKXU1z3WdIKbrieiwSDIWg88RD5flreeTDaCzrlgfXyNybi4UTUshbeo6SdkRiGs="
],
"n": "wUvZ-4dkT2nTfCDIwyH9K0tH4qYMGcW_KDYeh-TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh_TQ_8M_aJ_Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA-usFAfixPnU5L5lyacj3t-dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ__TKkGadZxBo8FObdKuy7XrrOvug4FAKe-3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N-KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRw",
"e": "AQAB",
"kid": "QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ",
"x5t": "QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ"
}
]
}
复制代码
关于这种格式的一些细节:kid
表明密钥标识符,而 x5c
属性是公钥自己(它是 x509 证书链)。
再次强调,咱们没必要要编写代码来使用这种格式,可是咱们须要对这个 REST 端点中发生的事情有一点了解:他只是简单地发布一个公钥。
node-jwks-rsa
库实现 JWT 密钥轮换因为公钥的格式是标准化的,咱们须要的是一种读取密钥的方法,并将其传递给 express-jwt
,如此以便它能够代替从文件系统中读取出来的公钥。
而这正是 node-jwks-rsa 库让咱们作的!咱们来看看这个库的运做:
const jwksRsa = require('jwks-rsa');
const expressJwt = require('express-jwt');
const checkIfAuthenticated = expressJwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksUri: "https://angularuniv-security-course.auth0.com/.well-known/jwks.json"
}),
algorithms: ['RS256']
});
app.route('/api/lessons')
.get(checkIfAuthenticated, readAllLessons);
复制代码
这个库经过 jwksUri
属性指定 URL 读取公钥,并使用其验证 JWT 签名。咱们须要作的只是匹配网址,若是须要的话还须要设置一些额外参数。
建议将 cache
属性设置为 true,以防每次都检索公钥。默认状况下,一个密钥会保留 10 小时,而后再检查它是否有效,同时最多缓存 5 个密钥。
rateLimit
属性也应该被启用,以确保库每分钟不会向包含公钥服务器发起超过 10 个请求。
这是为了不出现拒绝服务的状况,因为某种状况(包括攻击,但也许是一个 bug),公共服务器会不断进行公钥轮换。
这将使应用服务器很快中止,由于它有很好的内置防护措施!若是你想要更改这些默认参数,请查看库文档来获取更多详细信息。
这样,咱们已经完成了 JWT 的网络之旅!
咱们已经讨论了这个往返过程当中涉及到的多个设计方案。让咱们总结一下咱们所学到的。
将认证和受权等安全功能委派给第三方基于 JWT 的提供商或者产品比以往更加合适,但这并不意味着安全性能够透明地添加到应用中。
即便咱们选择了第三方认证提供商或企业级单点登陆解决方案,若是没有其余能够用来理解咱们所选的产品或者库的文档,咱们至少也要知道其中关于 JWT 的一些处理细节。
咱们仍然须要本身作不少安全设计方案,选择库和产品,选择关键配置选项,如 JWT 签名类型,设置托管登陆页面(若是可用),并放置一些很是关键的、很容易出错安全相关代码。
但愿这篇文章对你有帮助而且你能喜欢它!若是您有任何问题或者意见,请在下面的评论区告诉我,我将尽快回复您。
若是有更多的贴子发布,咱们将通知你订阅咱们的新闻列表。
爆破 HS256 是可能的: 使用强密钥在签署 JWT 的重要性
看看 Angular 大学的 Youtube 频道,咱们发布了大约 25% 到三分之一的视频教程,新视频一直在出版。
订阅 获取新的视频教程:
一样能够看看其余很受欢迎的帖子,你可能会以为有趣:
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。