Supercharge Java Authentication with JSON Web Tokens (JWTs) – 用JSON网络令牌(JWTs)为Java认证增效

最后修改: 2016年 7月 18日

中文/混合/英文(键盘快捷键:t)

Getting ready to build, or struggling with, secure authentication in your Java application? Unsure of the benefits of using tokens (and specifically JSON web tokens), or how they should be deployed? I’m excited to answer these questions, and more, for you in this tutorial!

准备在您的Java应用程序中建立安全认证,或者正在为之奋斗?不确定使用令牌(特别是JSON网络令牌)的好处,或应该如何部署?我很高兴能在本教程中回答这些问题,以及更多的问题。

Before we dive into JSON Web Tokens (JWTs), and the JJWT library (created by Stormpath’s CTO, Les Hazlewood and maintained by a community of contributors), let’s cover some basics.

在我们深入了解JSON网络令牌(JWTs),以及JWT 库(由 Stormpath 的首席技术官 Les Hazlewood 创建,并由贡献者社区维护),让我们来介绍一些基础知识。

1. Authentication vs. Token Authentication

1.认证与令牌认证

The set of protocols an application uses to confirm user identity is authentication. Applications have traditionally persisted identity through session cookies. This paradigm relies on server-side storage of session IDs which forces developers to create session storage that is either unique and server-specific, or implemented as a completely separate session storage layer.

应用程序用来确认用户身份的一组协议就是认证。传统上,应用程序通过会话cookies来保持身份。这种模式依赖于服务器端对会话ID的存储,这迫使开发者创建会话存储,这种存储要么是唯一的、服务器特定的,要么是作为一个完全独立的会话存储层实现。

Token authentication was developed to solve problems server-side session IDs didn’t, and couldn’t. Just like traditional authentication, users present verifiable credentials, but are now issued a set of tokens instead of a session ID. The initial credentials could be the standard username/password pair, API keys, or even tokens from another service. (Stormpath’s API Key Authentication Feature is an example of this.)

代币认证的开发是为了解决服务器端的会话ID没有,也不能解决的问题。就像传统的认证一样,用户出示可验证的凭证,但现在发给一组令牌而不是会话ID。最初的凭证可以是标准的用户名/密码对、API密钥,甚至是来自其他服务的令牌。(Stormpath的API密钥认证功能就是这样一个例子)。

1.1. Why Tokens?

1.1.为什么是代币?

Very simply, using tokens in place of session IDs can lower your server load, streamline permission management, and provide better tools for supporting a distributed or cloud-based infrastructure. In the case of JWT, this is primarily accomplished through the stateless nature of these types of tokens (more on that below).

很简单,使用令牌来代替会话ID可以降低你的服务器负载,简化权限管理,并提供更好的工具来支持分布式或基于云的基础设施。就JWT而言,这主要是通过这些类型的令牌的无状态性质来实现的(下文有更多介绍)。

Tokens offer a wide variety of applications, including: Cross Site Request Forgery (CSRF) protection schemes, OAuth 2.0 interactions, session IDs, and (in cookies) as authentication representations. In most cases, standards do not specify a particular format for tokens. Here’s an example of a typical Spring Security CSRF token in an HTML form:

令牌提供了广泛的应用,包括。跨网站请求伪造(CSRF)保护方案、OAuth 2.0交互、会话ID,以及(在cookie中)作为认证代表。在大多数情况下,标准并没有规定令牌的特定格式。下面是一个典型的Spring Security CSRF令牌在HTML表单中的例子。

<input name="_csrf" type="hidden" 
  value="f3f42ea9-3104-4d13-84c0-7bcb68202f16"/>

If you try to post that form without the right CSRF token, you get an error response, and that’s the utility of tokens. The above example is a “dumb” token. This means there is no inherent meaning to be gleaned from the token itself. This is also where JWTs make a big difference.

如果你试图在没有正确的CSRF令牌的情况下发布该表单,你会得到一个错误响应,这就是令牌的效用。上面的例子是一个 “哑巴 “令牌。这意味着从令牌本身没有内在的意义可言。这也是JWTs大有作为的地方。

2. What’s in a JWT?

2.JWT的内容是什么?

JWTs (pronounced “jots”) are URL-safe, encoded, cryptographically signed (sometimes encrypted) strings that can be used as tokens in a variety of applications. Here’s an example of a JWT being used as a CSRF token:

JWT(发音为 “jots”)是对URL安全的、经过编码的、经过密码学签名的(有时是加密的)字符串,可以在各种应用中作为令牌使用。下面是一个JWT被用作CSRF令牌的例子。

<input name="_csrf" type="hidden" 
  value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdCI6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9.rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc"/>

In this case, you can see that the token is much longer than in our previous example. Just like we saw before, if the form is submitted without the token you get an error response.

在这种情况下,你可以看到令牌比我们之前的例子要长很多。就像我们之前看到的那样,如果在没有令牌的情况下提交表单,你会得到一个错误响应。

So, why JWT?

那么,为什么是JWT?

The above token is cryptographically signed and therefore can be verified, providing proof that it hasn’t been tampered with. Also, JWTs are encoded with a variety of additional information.

上述令牌是经过加密签名的,因此可以被验证,提供没有被篡改的证据。另外,JWTs是用各种附加信息进行编码的。

Let’s look at the anatomy of a JWT to better understand how we squeeze all this goodness out of it. You may have noticed that there are three distinct sections separated by periods (.):

让我们看看JWT的解剖结构,以更好地理解我们是如何从它身上榨取所有这些好处的。你可能已经注意到,有三个不同的部分被句号(.)分开。

Header eyJhbGciOiJIUzI1NiJ9
Payload eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC
I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9
Signature rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Each section is base64 URL-encoded. This ensures that it can be used safely in a URL (more on this later). Let’s take a closer look at each section individually.

每个部分都是base64的URL编码。这确保了它可以在URL中安全地使用(后面会有更多介绍)。让我们仔细看看每个部分。

2.1. The Header

2.1.页眉

If you base64 to decode the header, you will get the following JSON string:

如果你用base64来解码这个头,你会得到以下JSON字符串。

{"alg":"HS256"}

This shows that the JWT was signed with HMAC using SHA-256.

这表明JWT是用HMAC签署的,使用SHA-256

2.2. The Payload

2.2.有效载荷

If you decode the payload, you get the following JSON string (formatted for clarity):

如果你对有效载荷进行解码,你会得到以下JSON字符串(为清晰起见,进行了格式化)。

{
  "jti": "e678f23347e3410db7e68767823b2d70",
  "iat": 1466633317,
  "nbf": 1466633317,
  "exp": 1466636917
}

Within the payload, as you can see, there are a number of keys with values. These keys are called “claims” and the JWT specification has seven of these specified as “registered” claims. They are:

正如你所看到的,在有效载荷中,有许多带值的键。这些键被称为 “索赔”,JWT规范将其中七个键指定为 “注册 “索赔。它们是

iss Issuer
sub Subject
aud Audience
exp Expiration
nbf Not Before
iat Issued At
jti JWT ID

When building a JWT, you can put in any custom claims you wish. The list above simply represents the claims that are reserved both in the key that is used and the expected type. Our CSRF has a JWT ID, an “Issued At” time, a “Not Before” time, and an Expiration time. The expiration time is exactly one minute past the issued at time.

在构建JWT时,你可以放入任何你想要的自定义声明。上面的列表只是代表了在使用的密钥和预期类型中保留的要求。我们的CSRF有一个JWT ID,一个 “签发时间”,一个 “非之前 “的时间,以及一个过期时间。过期时间正好是发出时间的1分钟。

2.3. The Signature

2.3.签名

Finally, the signature section is created by taking the header and payload together (with the . in between) and passing it through the specified algorithm (HMAC using SHA-256, in this case) along with a known secret. Note that the secret is always a byte array, and should be of a length that makes sense for the algorithm used. Below, I use a random base64 encoded string (for readability) that’s converted into a byte array.

最后,签名部分是通过把头和有效载荷放在一起(中间有.),并通过指定的算法(本例中是使用SHA-256的HMAC)和一个已知的秘密来创建。请注意,秘密总是一个字节数组,其长度应该对所使用的算法有意义。下面,我使用了一个随机的base64编码的字符串(为了可读性),它被转换为一个字节数组。

It looks like this in pseudo-code:

它在伪代码中看起来是这样的。

computeHMACSHA256(
    header + "." + payload, 
    base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)

As long as you know the secret, you can generate the signature yourself and compare your result to the signature section of the JWT to verify that it has not been tampered with. Technically, a JWT that’s been cryptographically signed is called a JWS. JWTs can also be encrypted and would then be called a JWE. (In actual practice, the term JWT is used to describe JWEs and JWSs.)

只要你知道这个秘密,你就可以自己生成签名,并将你的结果与JWT的签名部分进行比较,以验证它没有被篡改过。从技术上讲,经过加密签名的JWT被称为JWS>。JWT也可以被加密,然后被称为JWE>。(在实际操作中,JWT一词被用来描述JWE和JWS)。

This brings us back to the benefits of using a JWT as our CSRF token. We can verify the signature and we can use the information encoded in the JWT to confirm its validity. So, not only does the string representation of the JWT need to match what’s stored server-side, we can ensure that it’s not expired simply by inspecting the exp claim. This saves the server from maintaining additional state.

这让我们回到了使用JWT作为我们的CSRF令牌的好处。我们可以验证签名,我们可以使用JWT中编码的信息来确认其有效性。因此,不仅JWT的字符串表示需要与服务器端存储的内容相匹配,我们还可以通过检查exp声明来确保它没有过期。这使服务器无需维护额外的状态。

Well, we’ve covered a lot of ground here. Let’s dive into some code!

好了,我们在这里已经覆盖了很多地方。让我们深入学习一些代码吧!

3. Setup the JJWT Tutorial

3.设置JJWT教程

JJWT (https://github.com/jwtk/jjwt) is a Java library providing end-to-end JSON Web Token creation and verification. Forever free and open-source (Apache License, Version 2.0), it was designed with a builder-focused interface hiding most of its complexity.

JJWT(https://github.com/jwtk/jjwt)是一个提供端到端JSON网络令牌创建和验证的Java库。它永远是免费和开源的(Apache许可证,2.0版),它被设计成以构建者为中心的界面,隐藏了其大部分的复杂性。

The primary operations in using JJWT involve building and parsing JWTs. We’ll look at these operations next, then get into some extended features of the JJWT, and finally, we’ll see JWTs in action as CSRF tokens in a Spring Security, Spring Boot application.

使用JJWT的主要操作涉及构建和解析JWT。我们接下来会看一下这些操作,然后讨论JJWT的一些扩展功能,最后,我们会看到JWT在Spring Security和Spring Boot应用中作为CSRF令牌发挥作用。

The code demonstrated in the following sections can be found here. Note: The project uses Spring Boot from the beginning as its easy to interact with the API that it exposes.

以下部分演示的代码可以在这里找到。注意:该项目从一开始就使用Spring Boot,因为它很容易与它所暴露的API进行交互。

One of the great things about Spring Boot is how easy it is to build and fire up an application. To run the JJWT Fun application, simply do the following:

Spring Boot的一大优点是可以很容易地构建和启动一个应用程序。要运行JJWT Fun应用程序,只需进行以下操作。

mvn clean spring-boot:run

There are ten endpoints exposed in this example application (I use httpie to interact with the application. It can be found here.)

在这个例子中,有十个端点暴露出来(我使用httpie与应用程序进行交互。它可以找到这里)。

http localhost:8080
Available commands (assumes httpie - https://github.com/jkbrzt/httpie):

  http http://localhost:8080/
	This usage message

  http http://localhost:8080/static-builder
	build JWT from hardcoded claims

  http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
	build JWT from passed in claims (using general claims map)

  http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
	build JWT from passed in claims (using specific claims methods)

  http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
	build DEFLATE compressed JWT from passed in claims

  http http://localhost:8080/parser?jwt=<jwt>
	Parse passed in JWT

  http http://localhost:8080/parser-enforce?jwt=<jwt>
	Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim

  http http://localhost:8080/get-secrets
	Show the signing keys currently in use.

  http http://localhost:8080/refresh-secrets
	Generate new signing keys and show them.

  http POST http://localhost:8080/set-secrets 
    HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value
	Explicitly set secrets to use in the application.

In the sections that follow, we will examine each of these endpoints and the JJWT code contained in the handlers.

在接下来的章节中,我们将检查这些端点中的每一个以及处理程序中包含的JJWT代码。

4. Building JWTs With JJWT

4.用JJWT构建JWT

Because of JJWT’s fluent interface, the creation of the JWT is basically a three-step process:

由于JJWT的流畅的界面,JWT的创建基本上是一个三步过程。

  1. The definition of the internal claims of the token, like Issuer, Subject, Expiration, and ID.
  2. The cryptographic signing of the JWT (making it a JWS).
  3. The compaction of the JWT to a URL-safe string, according to the JWT Compact Serialization rules.

The final JWT will be a three-part base64-encoded string, signed with the specified signature algorithm, and using the provided key. After this point, the token is ready to be shared with the another party.

最终的JWT将是一个由三部分组成的base64编码的字符串,用指定的签名算法签署,并使用提供的密钥。在这一点上,令牌已经准备好与另一方共享。

Here’s an example of the JJWT in action:

这里有一个JJWT的行动实例。

String jws = Jwts.builder()
  .setIssuer("Stormpath")
  .setSubject("msilverman")
  .claim("name", "Micah Silverman")
  .claim("scope", "admins")
  // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
  .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))
  // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
  .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
  .signWith(
    SignatureAlgorithm.HS256,
    TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
  )
  .compact();

This is very similar to the code that’s in the StaticJWTController.fixedBuilder method of the code project.

这与代码项目的StaticJWTController.fixedBuilder方法中的代码非常相似。

At this point, it’s worth talking about a few anti-patterns related to JWTs and signing. If you’ve ever seen JWT examples before, you’ve likely encountered one of these signing anti-pattern scenarios:

在这一点上,值得谈一谈与JWT和签名有关的一些反模式。如果你以前看过JWT的例子,你很可能遇到过这些签名反模式的情况之一。

  1. .signWith(
        SignatureAlgorithm.HS256,
       "secret".getBytes("UTF-8")    
    )
  2. .signWith(
        SignatureAlgorithm.HS256,
        "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8")
    )
  3. .signWith(
        SignatureAlgorithm.HS512,
        TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
    )

Any of the HS type signature algorithms takes a byte array. It’s convenient for humans to read to take a string and convert it to a byte array.

任何一种HS类型的签名算法都需要一个字节数组。人类阅读时,取一个字符串并将其转换为一个字节数组是很方便的。

Anti-pattern 1 above demonstrates this. This is problematic because the secret is weakened by being so short and it’s not a byte array in its native form. So, to keep it readable, we can base64 encode the byte array.

上面的反模式1证明了这一点。这是有问题的,因为秘密由于太短而被削弱了,而且它不是一个字节数组的原始形式。因此,为了保持其可读性,我们可以对字节数组进行base64编码。

However, anti-pattern 2 above takes the base64 encoded string and converts it directly to a byte array. What should be done is to decode the base64 string back into the original byte array.

然而,上面的反模式2将base64编码的字符串直接转换为字节数组。应该做的是将base64字符串解码回原始字节数组。

Number 3 above demonstrates this. So, why is this one also an anti-pattern? It’s a subtle reason in this case. Notice that the signature algorithm is HS512. The byte array is not the maximum length that HS512 can support, making it a weaker secret than what is possible for that algorithm.

上面的第3条证明了这一点。那么,为什么这个也是一个反模式呢?在这种情况下,这是一个微妙的原因。请注意,签名算法是HS512。字节数组不是HS512所能支持的最大长度,使得它的秘密比该算法所能支持的要弱。

The example code includes a class called SecretService that ensures secrets of the proper strength are used for the given algorithm. At application startup time, a new set of secrets is created for each of the HS algorithms. There are endpoints to refresh the secrets as well as to explicitly set the secrets.

示例代码包括一个名为SecretService的类,它确保适当强度的秘密被用于给定的算法。在应用程序启动时,将为每个HS算法创建一套新的秘密。有一些端点可以刷新秘密,也可以明确设置秘密。

If you have the project running as described above, execute the following so that the JWT examples below match the responses from your project.

如果你有如上所述的项目在运行,执行以下内容,使下面的JWT例子与你的项目的响应相符。

http POST localhost:8080/set-secrets \