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.
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的创建基本上是一个三步过程。
- The definition of the internal claims of the token, like Issuer, Subject, Expiration, and ID.
- The cryptographic signing of the JWT (making it a JWS).
- 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的例子,你很可能遇到过这些签名反模式的情况之一。
-
.signWith( SignatureAlgorithm.HS256, "secret".getBytes("UTF-8") )
-
.signWith( SignatureAlgorithm.HS256, "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8") )
-
.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 \
HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="
Now, you can hit the /static-builder endpoint:
现在,你可以点击/static-builder端点。
http http://localhost:8080/static-builder
This produces a JWT that looks like this:
这将产生一个看起来像这样的JWT。
eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ
Now, hit:
现在,打。
http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ
The response has all the claims that we included when we created the JWT.
响应中有我们在创建JWT时包含的所有要求。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
"jws": {
"body": {
"exp": 4622470422,
"iat": 1466796822,
"iss": "Stormpath",
"name": "Micah Silverman",
"scope": "admins",
"sub": "msilverman"
},
"header": {
"alg": "HS256"
},
"signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
},
"status": "SUCCESS"
}
This is the parsing operation, which we’ll get into in the next section.
这是解析操作,我们将在下一节讨论。
Now, let’s hit an endpoint that takes claims as parameters and will build a custom JWT for us.
现在,让我们打一个端点,它接受索赔作为参数,并将为我们建立一个自定义的JWT。
http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true
Note: There’s a subtle difference between the hasMotorcycle claim and the other claims. httpie assumes that JSON parameters are strings by default. To submit raw JSON using using httpie, you use the := form rather than =. Without that, it would submit “hasMotorcycle”: “true”, which is not what we want.
注意:hasMotorcycle索赔和其他索赔之间有一个微妙的区别。 httpie默认假设JSON参数是字符串。要使用httpie提交原始JSON,你需要使用 :=形式,而不是=。不这样做,它就会提交“hasMotorcycle”:”true”,这不是我们想要的。
Here’s the output:
下面是输出结果。
POST /dynamic-builder-general HTTP/1.1
Accept: application/json
...
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": "msilverman"
}
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
"jwt":
"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU",
"status": "SUCCESS"
}
Let’s take a look at the code that backs this endpoint:
让我们看一下支持这个端点的代码。
@RequestMapping(value = "/dynamic-builder-general", method = POST)
public JwtResponse dynamicBuilderGeneric(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
String jws = Jwts.builder()
.setClaims(claims)
.signWith(
SignatureAlgorithm.HS256,
secretService.getHS256SecretBytes()
)
.compact();
return new JwtResponse(jws);
}
Line 2 ensures that the incoming JSON is automatically converted to a Java Map<String, Object>, which is super handy for JJWT as the method on line 5 simply takes that Map and sets all the claims at once.
第2行确保传入的JSON被自动转换为Java Map
As terse as this code is, we need something more specific to ensure that the claims that are passed are valid. Using the .setClaims(Map<String, Object> claims) method is handy when you already know that the claims represented in the map are valid. This is where the type-safety of Java comes into the JJWT library.
尽管这段代码很简洁,但我们需要更具体的东西来确保传递的索赔是有效的。当你已经知道地图中代表的索赔是有效的时候,使用.setClaims(Map<String, Object> claims)方法很方便。这就是Java的类型安全在JJWT库中的体现。
For each of the Registered Claims defined in the JWT specification, there’s a corresponding Java method in the JJWT that takes the spec-correct type.
对于JWT规范中定义的每一个注册索赔,JJWT中都有一个相应的Java方法,该方法采取规范正确的类型。
Let’s hit another endpoint in our example and see what happens:
让我们在我们的例子中点击另一个端点,看看会发生什么。
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
Note that we’ve passed in an integer, 5, for the “sub” claim. Here’s the output:
请注意,我们为 “子 “的要求传入了一个整数,5。下面是输出结果。
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": 5
}
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "java.lang.ClassCastException",
"message": "java.lang.Integer cannot be cast to java.lang.String",
"status": "ERROR"
}
Now, we’re getting an error response because the code is enforcing the type of the Registered Claims. In this case, sub must be a string. Here’s the code that backs this endpoint:
现在,我们得到了一个错误响应,因为代码正在强制执行注册索赔的类型。在这种情况下,sub必须是一个字符串。下面是支持这个端点的代码。
@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
JwtBuilder builder = Jwts.builder();
claims.forEach((key, value) -> {
switch (key) {
case "iss":
builder.setIssuer((String) value);
break;
case "sub":
builder.setSubject((String) value);
break;
case "aud":
builder.setAudience((String) value);
break;
case "exp":
builder.setExpiration(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "nbf":
builder.setNotBefore(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "iat":
builder.setIssuedAt(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "jti":
builder.setId((String) value);
break;
default:
builder.claim(key, value);
}
});
builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());
return new JwtResponse(builder.compact());
}
Just like before, the method accepts a Map<String, Object> of claims as its parameter. However, this time, we are calling the specific method for each of the Registered Claims which enforces type.
就像以前一样,该方法接受一个Map<String, Object>的索赔作为其参数。然而,这一次,我们正在为每一个注册索赔调用特定的方法,该方法强制执行类型。
One refinement to this is to make the error message more specific. Right now, we only know that one of our claims is not the correct type. We don’t know which claim was in error or what it should be. Here’s a method that will give us a more specific error message. It also deals with a bug in the current code.
这方面的一个改进是使错误信息更加具体。现在,我们只知道我们的一个索赔不是正确的类型。我们不知道哪个索赔出了错,也不知道它应该是什么。这里有一个方法,可以给我们一个更具体的错误信息。它还处理了当前代码中的一个错误。
private void ensureType(String registeredClaim, Object value, Class expectedType) {
boolean isCorrectType =
expectedType.isInstance(value) ||
expectedType == Long.class && value instanceof Integer;
if (!isCorrectType) {
String msg = "Expected type: " + expectedType.getCanonicalName() +
" for registered claim: '" + registeredClaim + "', but got value: " +
value + " of type: " + value.getClass().getCanonicalName();
throw new JwtException(msg);
}
}
Line 3 checks that the passed in value is of the expected type. If not, a JwtException is thrown with the specific error. Let’s take a look at this in action by making the same call we did earlier:
第3行检查传入的值是否是预期的类型。如果不是,就会抛出一个JwtException,并指出具体的错误。让我们通过先前的调用来看看这个动作。
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
User-Agent: HTTPie/0.9.3
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": 5
}
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "io.jsonwebtoken.JwtException",
"message":
"Expected type: java.lang.String for registered claim: 'sub', but got value: 5 of type: java.lang.Integer",
"status": "ERROR"
}
Now, we have a very specific error message telling us that the sub claim is the one in error.
现在,我们有一个非常具体的错误信息,告诉我们sub索赔是一个错误。
Let’s circle back to that bug in our code. The issue has nothing to do with the JJWT library. The issue is that the JSON to Java Object mapper built into Spring Boot is too smart for our own good.
让我们回到我们代码中的那个错误。这个问题与JJWT库没有关系。问题在于,Spring Boot中内置的JSON到Java对象的映射器对我们自己来说太聪明了。
If there’s a method that accepts a Java Object, the JSON mapper will automatically convert a passed in number that is less than or equal to 2,147,483,647 into a Java Integer. Likewise, it will automatically convert a passed in number that is greater than 2,147,483,647 into a Java Long. For the iat, nbf, and exp claims of a JWT, we want our ensureType test to pass whether the mapped Object is an Integer or a Long. That’s why we have the additional clause in determining if the passed in value is the correct type:
如果有一个接受Java对象的方法,JSON映射器将自动把一个小于或等于2,147,483,647的传入数转换成Java Integer。同样地,它将自动把大于2,147,483,647的传入数字转换为Java Long。对于JWT的iat、nbf和exp请求,我们希望我们的ensureType测试能够通过,无论映射的对象是整数还是长数。这就是为什么我们在确定传入的值是否是正确的类型时有了额外的条款。
boolean isCorrectType =
expectedType.isInstance(value) ||
expectedType == Long.class && value instanceof Integer;
If we’re expecting a Long, but the value is an instance of Integer, we still say it’s the correct type. With an understanding of what’s happening with this validation, we can now integrate it into our dynamicBuilderSpecific method:
如果我们期待的是一个Long,但其值是Integer的一个实例,我们仍然说它是正确的类型。在了解了这个验证所发生的事情后,我们现在可以把它整合到我们的dynamicBuilderSpecific方法中。
@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
JwtBuilder builder = Jwts.builder();
claims.forEach((key, value) -> {
switch (key) {
case "iss":
ensureType(key, value, String.class);
builder.setIssuer((String) value);
break;
case "sub":
ensureType(key, value, String.class);
builder.setSubject((String) value);
break;
case "aud":
ensureType(key, value, String.class);
builder.setAudience((String) value);
break;
case "exp":
ensureType(key, value, Long.class);
builder.setExpiration(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "nbf":
ensureType(key, value, Long.class);
builder.setNotBefore(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "iat":
ensureType(key, value, Long.class);
builder.setIssuedAt(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "jti":
ensureType(key, value, String.class);
builder.setId((String) value);
break;
default:
builder.claim(key, value);
}
});
builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());
return new JwtResponse(builder.compact());
}
Note: In all the example code in this section, JWTs are signed with the HMAC using SHA-256 algorithm. This is to keep the examples simple. The JJWT library supports 12 different signature algorithms that you can take advantage of in your own code.
注意:在本节的所有示例代码中,JWTs是使用SHA-256算法的HMAC签名。这是为了保持例子的简单性。JJWT库支持12种不同的签名算法,你可以在自己的代码中加以利用。
5. Parsing JWTs With JJWT
5.用JJWT解析JWTs
We saw earlier that our code example has an endpoint for parsing a JWT. Hitting this endpoint:
我们在前面看到,我们的代码示例有一个用于解析JWT的端点。打击这个端点。
http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ
produces this response:
产生这种反应。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
"claims": {
"body": {
"exp": 4622470422,
"iat": 1466796822,
"iss": "Stormpath",
"name": "Micah Silverman",
"scope": "admins",
"sub": "msilverman"
},
"header": {
"alg": "HS256"
},
"signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
},
"status": "SUCCESS"
}
The parser method of the StaticJWTController class looks like this:
StaticJWTController类的parser方法看起来像这样。
@RequestMapping(value = "/parser", method = GET)
public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
Jws<Claims> jws = Jwts.parser()
.setSigningKeyResolver(secretService.getSigningKeyResolver())
.parseClaimsJws(jwt);
return new JwtResponse(jws);
}
Line 4 indicates that we expect the incoming string to be a signed JWT (a JWS). And, we are using the same secret that was used to sign the JWT in parsing it. Line 5 parses the claims from the JWT. Internally, it is verifying the signature and it will throw an exception if the signature is invalid.
第4行指出,我们期望传入的字符串是一个经过签名的JWT(JWS)。而且,我们在解析JWT时使用的是用于签名的相同的秘密。第5行解析了JWT中的索赔。在内部,它正在验证签名,如果签名无效,它将抛出一个异常。
Notice that in this case we are passing in a SigningKeyResolver rather than a key itself. This is one of the most powerful aspects of JJWT. The header of JWT indicates the algorithm used to sign it. However, we need to verify the JWT before we trust it. It would seem to be a catch 22. Let’s look at the SecretService.getSigningKeyResolver method:
请注意,在这种情况下,我们传递的是一个SigningKeyResolver,而不是一个密钥本身。这是JJWT最强大的方面之一。JWT的头显示了用于签名的算法。然而,在我们信任JWT之前,我们需要验证它。这似乎是一个陷阱。让我们看一下SecretService.getSigningKeyResolver方法。
private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
}
};
Using the access to the JwsHeader, I can inspect the algorithm and return the proper byte array for the secret that was used to sign the JWT. Now, JJWT will verify that the JWT has not been tampered with using this byte array as the key.
利用对JwsHeader的访问,我可以检查算法并返回用于签署JWT的秘密的适当字节数组。现在,JJWT将使用这个字节数作为密钥来验证JWT没有被篡改。
If I remove the last character of the passed in JWT (which is part of the signature), this is the response:
如果我去掉传入的JWT的最后一个字符(它是签名的一部分),这就是响应。
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 27 Jun 2016 13:19:08 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
{
"exceptionType": "io.jsonwebtoken.SignatureException",
"message":
"JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
"status": "ERROR"
}
6. JWTs in Practice: Spring Security CSRF Tokens
6.JWTs的实践 Spring Security CSRF Tokens
While the focus of this post is not Spring Security, we are going to delve into it a bit here to showcase some real-world usage of the JJWT library.
虽然这篇文章的重点不是Spring Security,但我们要在这里深入研究一下,以展示JJWT库的一些实际使用情况。
Cross Site Request Forgery is a security vulnerability whereby a malicious website tricks you into submitting requests to a website that you have established trust with. One of the common remedies for this is to implement a synchronizer token pattern. This approach inserts a token into the web form and the application server checks the incoming token against its repository to confirm that it is correct. If the token is missing or invalid, the server will respond with an error.
跨网站请求伪造是一种安全漏洞,恶意网站会欺骗您向您已经建立信任的网站提交请求。对此,常见的补救措施之一是实施同步器令牌模式。这种方法在Web表单中插入一个令牌,应用服务器根据其存储库检查传入的令牌,以确认其是否正确。如果令牌丢失或无效,服务器将回应一个错误。
Spring Security has the synchronizer token pattern built in. Even better, if you are using the Spring Boot and Thymeleaf templates, the synchronizer token is automatically inserted for you.
Spring Security已经内置了同步器令牌模式。更妙的是,如果您使用Spring Boot和Thymeleaf模板,同步器令牌将自动为您插入。
By default, the token that Spring Security uses is a “dumb” token. It’s just a series of letters and numbers. This approach is just fine and it works. In this section, we enhance the basic functionality by using JWTs as the token. In addition to verifying that the submitted token is the one expected, we validate the JWT to further prove that the token has not been tampered with and to ensure that it is not expired.
默认情况下,Spring Security使用的令牌是一个 “哑巴 “令牌。它只是一系列的字母和数字。这种方法很好,也很有效。在本节中,我们通过使用JWTs作为令牌来增强基本功能。除了验证提交的令牌是预期的令牌外,我们还验证JWT以进一步证明令牌没有被篡改,并确保它没有过期。
To get started, we are going to configure Spring Security using Java configuration. By default, all paths require authentication and all POST endpoints require CSRF tokens. We are going to relax that a bit so that what we’ve built so far still works.
为了开始,我们将使用Java配置来配置Spring Security。默认情况下,所有路径都需要认证,所有POST端点都需要CSRF令牌。我们将放宽一点,以便我们到目前为止建立的东西仍然可以使用。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private String[] ignoreCsrfAntMatchers = {
"/dynamic-builder-compress",
"/dynamic-builder-general",
"/dynamic-builder-specific",
"/set-secrets"
};
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers(ignoreCsrfAntMatchers)
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}
}
We are doing two things here. First, we are saying the CSRF tokens are not required when posting to our REST API endpoints (line 15). Second, we are saying that unauthenticated access should be allowed for all paths (lines 17 – 18).
我们在这里做了两件事。首先,我们说CSRF令牌在发布到我们的REST API端点时是不需要的(第15行)。第二,我们说所有路径都应允许未经认证的访问(第17-18行)。
Let’s confirm that Spring Security is working the way we expect. Fire up the app and hit this url in your browser:
让我们确认Spring Security正在以我们期望的方式工作。启动应用程序,在你的浏览器中点击这个网址。
http://localhost:8080/jwt-csrf-form
Here’s the Thymeleaf template for this view:
下面是这个视图的Thymeleaf模板。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!--/*/ <th:block th:include="fragments/head :: head"/> /*/-->
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="box col-md-6 col-md-offset-3">
<p/>
<form method="post" th:action="@{/jwt-csrf-form}">
<input type="submit" class="btn btn-primary" value="Click Me!"/>
</form>
</div>
</div>
</div>
</body>
</html>
This is a very basic form that will POST to the same endpoint when submitted. Notice that there is no explicit reference to CSRF tokens in the form. If you view the source, you will see something like:
这是一个非常基本的表单,当提交时将被POST到同一个端点。请注意,表单中没有明确提到CSRF令牌。如果你查看源代码,你会看到类似的东西。
<input type="hidden" name="_csrf" value="5f375db2-4f40-4e72-9907-a290507cb25e" />
This is all the confirmation you need to know that Spring Security is functioning and that the Thymeleaf templates are automatically inserting the CSRF token.
这就是你需要知道的所有确认,即Spring Security正在运作,Thymeleaf模板正在自动插入CSRF令牌。
To make the value a JWT, we will enable a custom CsrfTokenRepository. Here’s how our Spring Security configuration changes:
为了使该值成为JWT,我们将启用一个自定义的CsrfTokenRepository。下面是我们的Spring Security配置的变化。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CsrfTokenRepository jwtCsrfTokenRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(jwtCsrfTokenRepository)
.ignoringAntMatchers(ignoreCsrfAntMatchers)
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}
}
To connect this, we need a configuration that exposes a bean that returns the custom token repository. Here’s the configuration:
为了连接这个,我们需要一个配置,暴露一个返回自定义令牌库的bean。这里是配置。
@Configuration
public class CSRFConfig {
@Autowired
SecretService secretService;
@Bean
@ConditionalOnMissingBean
public CsrfTokenRepository jwtCsrfTokenRepository() {
return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
}
}
And, here’s our custom repository (the important bits):
还有,这里是我们的自定义资源库(重要的部分)。
public class JWTCsrfTokenRepository implements CsrfTokenRepository {
private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
private byte[] secret;
public JWTCsrfTokenRepository(byte[] secret) {
this.secret = secret;
}
@Override
public CsrfToken generateToken(HttpServletRequest request) {
String id = UUID.randomUUID().toString().replace("-", "");
Date now = new Date();
Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds
String token;
try {
token = Jwts.builder()
.setId(id)
.setIssuedAt(now)
.setNotBefore(now)
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
} catch (UnsupportedEncodingException e) {
log.error("Unable to create CSRf JWT: {}", e.getMessage(), e);
token = id;
}
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
...
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
...
}
}
The generateToken method creates a JWT that expires 30 seconds after it’s created. With this plumbing in place, we can fire up the application again and look at the source of /jwt-csrf-form.
generateToken方法创建了一个JWT,在创建30秒后失效。有了这个管道,我们可以再次启动应用程序并查看/jwt-csrf-form的源代码。
Now, the hidden field looks like this:
现在,隐藏字段看起来像这样。
<input type="hidden" name="_csrf"
value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxZjIyMDdiNTVjOWM0MjU0YjZlMjY4MjQwYjIwNzZkMSIsImlhdCI6MTQ2NzA3MDQwMCwibmJmIjoxNDY3MDcwNDAwLCJleHAiOjE0NjcwNzA0MzB9.2kYLO0iMWUheAncXAzm0UdQC1xUC5I6RI_ShJ_74e5o" />
Huzzah! Now our CSRF token is a JWT. That wasn’t too hard.
Huzzah!现在我们的CSRF令牌是一个JWT。这并不难。
However, this is only half the puzzle. By default, Spring Security simply saves the CSRF token and confirms that the token submitted in a web form matches the one that’s saved. We want to extend the functionality to validate the JWT and make sure it hasn’t expired. To do that, we’ll add in a filter. Here’s what our Spring Security configuration looks like now:
然而,这只是问题的一半。默认情况下,Spring Security只是保存CSRF令牌,并确认在Web表单中提交的令牌与保存的令牌相符。我们想扩展该功能,以验证JWT并确保它没有过期。为了做到这一点,我们将添加一个过滤器。下面是我们的Spring Security配置现在的样子。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
.csrf()
.csrfTokenRepository(jwtCsrfTokenRepository)
.ignoringAntMatchers(ignoreCsrfAntMatchers)
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}
...
}
On line 9, we’ve added in a filter and we are placing it in the filter chain after the default CsrfFilter. So, by the time our filter is hit, the JWT token (as a whole) will have already been confirmed to be the correct value saved by Spring Security.
在第9行,我们添加了一个过滤器,并把它放在默认CsrfFilter之后的过滤器链中。因此,当我们的过滤器被击中时,JWT令牌(作为一个整体)将已经被确认为Spring Security保存的正确值。
Here’s the JwtCsrfValidatorFilter (it’s private as it’s an inner class of our Spring Security configuration):
这是JwtCsrfValidatorFilter(它是私有的,因为它是我们Spring Security配置的一个内部类)。
private class JwtCsrfValidatorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// NOTE: A real implementation should have a nonce cache so the token cannot be reused
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
if (
// only care if it's a POST
"POST".equals(request.getMethod()) &&
// ignore if the request path is in our list
Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
// make sure we have a token
token != null
) {
// CsrfFilter already made sure the token matched.
// Here, we'll make sure it's not expired
try {
Jwts.parser()
.setSigningKey(secret.getBytes("UTF-8"))
.parseClaimsJws(token.getToken());
} catch (JwtException e) {
// most likely an ExpiredJwtException, but this will handle any
request.setAttribute("exception", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
dispatcher.forward(request, response);
}
}
filterChain.doFilter(request, response);
}
}
Take a look at line 23 on. We are parsing the JWT as before. In this case, if an Exception is thrown, the request is forwarded to the expired-jwt template. If the JWT validates, then processing continues as normal.
请看第23行。我们正在像以前一样解析JWT。在这种情况下,如果抛出一个异常,请求将被转发到expired-jwt模板。如果JWT验证通过,则继续正常处理。
This closes the loop on overriding the default Spring Security CSRF token behavior with a JWT token repository and validator.
这就结束了用JWT令牌库和验证器覆盖默认的Spring Security CSRF令牌行为的循环。
If you fire up the app, browse to /jwt-csrf-form, wait a little more than 30 seconds and click the button, you will see something like this:
如果你启动应用程序,浏览到/jwt-csrf-form,等待30秒多一点,然后点击按钮,你会看到这样的东西。
7. JJWT Extended Features
7.JJWT的扩展功能
We’ll close out our JJWT journey with a word on some of the features that extend beyond the specification.
在结束我们的JJWT之旅时,我们将谈一谈规范之外的一些功能。
7.1. Enforce Claims
7.1.强制执行索赔
As part of the parsing process, JJWT allows you to specify required claims and values those claims should have. This is very handy if there is certain information in your JWTs that must be present in order for you to consider them valid. It avoids a lot of branching logic to manually validate claims. Here’s the method that serves the /parser-enforce endpoint of our sample project.
作为解析过程的一部分,JJWT允许你指定所需的索赔和这些索赔应具有的价值。如果你的JWTs中的某些信息必须存在,以便你认为它们是有效的,这就非常方便了。它避免了大量的分支逻辑来手动验证索赔。下面是为我们的示例项目的/parser-enforce端点服务的方法。
@RequestMapping(value = "/parser-enforce", method = GET)
public JwtResponse parserEnforce(@RequestParam String jwt)
throws UnsupportedEncodingException {
Jws<Claims> jws = Jwts.parser()
.requireIssuer("Stormpath")
.require("hasMotorcycle", true)
.setSigningKeyResolver(secretService.getSigningKeyResolver())
.parseClaimsJws(jwt);
return new JwtResponse(jws);
}
Lines 5 and 6 show you the syntax for registered claims as well as custom claims. In this example, the JWT will be considered invalid if the iss claim is not present or does not have the value: Stormpath. It will also be invalid if the custom hasMotorcycle claim is not present or does not have the value: true.
第5行和第6行向你展示了注册索赔以及自定义索赔的语法。在这个例子中,如果iss claim不存在或没有值,JWT将被认为是无效的。Stormpath。如果自定义的hasMotorcycle请求不存在或没有值:true,它也将是无效的。
Let’s first create a JWT that follows the happy path:
让我们首先创建一个遵循快乐路径的JWT。
http -v POST localhost:8080/dynamic-builder-specific \
iss=Stormpath hasMotorcycle:=true sub=msilverman
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": "msilverman"
}
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
"jwt":
"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0",
"status": "SUCCESS"
}
Now, let’s validate that JWT:
现在,让我们验证一下JWT。
http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0
GET /parser-enforce?jwt=http
-v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0 HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
"jws": {
"body": {
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": "msilverman"
},
"header": {
"alg": "HS256"
},
"signature": "qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0"
},
"status": "SUCCESS"
}
So far, so good. Now, this time, let’s leave the hasMotorcycle out:
到目前为止,一切都很好。现在,这一次,让我们把hasMotorcycle排除在外。
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman
This time, if we try to validate the JWT:
这一次,如果我们试图验证JWT。
http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc
we get:
我们得到。
GET /parser-enforce?jwt=http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "io.jsonwebtoken.MissingClaimException",
"message":
"Expected hasMotorcycle claim to be: true, but was not present in the JWT claims.",
"status": "ERROR"
}
This indicates that our hasMotorcycle claim was expected, but was missing.
这表明我们的hasMotorcycle要求是预期的,但却没有找到。
Let’s do one more example:
让我们再做一个例子。
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman
This time, the required claim is present, but it has the wrong value. Let’s see the output of:
这一次,所需的索赔是存在的,但它的值是错误的。让我们看看以下的输出。
http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c
GET /parser-enforce?jwt=http
-v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "io.jsonwebtoken.IncorrectClaimException",
"message": "Expected hasMotorcycle claim to be: true, but was: false.",
"status": "ERROR"
}
This indicates that our hasMotorcycle claim was present, but had a value that was not expected.
这表明我们的hasMotorcycle索赔是存在的,但有一个没有预期的值。
MissingClaimException and IncorrectClaimException are your friends when enforcing claims in your JWTs and a feature that only the JJWT library has.
MissingClaimException和IncorrectClaimException是你在JWTs中执行索赔的朋友,也是只有JJWT库才有的功能。
7.2. JWT Compression
7.2.JWT压缩
If you have a lot of claims on a JWT, it can get big – so big, that it might not fit in a GET url in some browsers.
如果你在JWT上有很多要求,它可能会变得很大–大到在某些浏览器中可能无法装入GET url。
Let’s a make a big JWT:
让我们做一个大的JWT。
http -v POST localhost:8080/dynamic-builder-specific \
iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
somewhere=over rainbow=way up=high and=the dreams=you dreamed=of
Here’s the JWT that produces:
下面是产生的JWT。
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA
That sucker’s big! Now, let’s hit a slightly different endpoint with the same claims:
这东西真大啊!现在,让我们用同样的要求来打一个稍微不同的终点。
http -v POST localhost:8080/dynamic-builder-compress \
iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
somewhere=over rainbow=way up=high and=the dreams=you dreamed=of
This time, we get:
这一次,我们得到了。
eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE
62 characters shorter! Here’s the code for the method used to generate the JWT:
缩短了62个字符!下面是用于生成JWT的方法的代码。
@RequestMapping(value = "/dynamic-builder-compress", method = POST)
public JwtResponse dynamicBuildercompress(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
String jws = Jwts.builder()
.setClaims(claims)
.compressWith(CompressionCodecs.DEFLATE)
.signWith(
SignatureAlgorithm.HS256,
secretService.getHS256SecretBytes()
)
.compact();
return new JwtResponse(jws);
}
Notice on line 6 we are specifying a compression algorithm to use. That’s all there is to it.
注意在第6行,我们指定了一个要使用的压缩算法。这就是它的全部内容。
What about parsing compressed JWTs? The JJWT library automatically detects the compression and uses the same algorithm to decompress:
如何解析压缩的JWTs?JJWT库会自动检测压缩,并使用相同的算法来解压。
GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
"claims": {
"body": {
"and": "the",
"brown": "fox",
"dreamed": "of",
"dreams": "you",
"hasMotorcycle": true,
"iss": "Stormpath",
"jumped": "over",
"lazy": "dog",
"rainbow": "way",
"somewhere": "over",
"sub": "msilverman",
"the": "quick",
"up": "high"
},
"header": {
"alg": "HS256",
"calg": "DEF"
},
"signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"
},
"status": "SUCCESS"
}
Notice the calg claim in the header. This was automatically encoded into the JWT and it provides the hint to the parser about what algorithm to use for decompression.
注意头中的calg要求。这是在JWT中自动编码的,它向解析器提供了关于使用何种算法进行解压的提示。
NOTE: The JWE specification does support compression. In an upcoming release of the JJWT library, we will support JWE and compressed JWEs. We will continue to support compression in other types of JWTs, even though it is not specified.
注意:JWE规范确实支持压缩。在即将发布的JJWT库中,我们将支持JWE和压缩的JWEs。我们将继续支持其他类型的JWTs的压缩,尽管它没有被指定。
8. Token Tools for Java Devs
8.面向Java开发者的令牌工具
While the core focus of this article was not Spring Boot or Spring Security, using those two technologies made it easy to demonstrate all the features discussed in this article. You should be able to build in fire up the server and start playing with the various endpoints we’ve discussed. Just hit:
虽然本文的核心重点不是Spring Boot或Spring Security,但使用这两种技术可以很容易地演示本文讨论的所有功能。你应该能够建立并启动服务器,开始玩我们讨论过的各种端点。就打。
http http://localhost:8080
Stormpath is also excited to bring a number of open source developer tools to the Java community. These include:
Stormpath也很高兴能够为Java社区带来一些开源的开发工具。这些工具包括
8.1. JJWT (What We’ve Been Talking About)
8.1.JJWT(我们一直在谈论的东西)
JJWT is an easy to use tool for developers to create and verify JWTs in Java. Like many libraries Stormpath supports, JJWT is completely free and open source (Apache License, Version 2.0), so everyone can see what it does and how it does it. Do not hesitate to report any issues, suggest improvements, and even submit some code!
JJWT是一个易于使用的工具,供开发人员在Java中创建和验证JWT。与Stormpath支持的许多库一样,JJWT是完全免费和开源的(Apache许可证,2.0版),因此每个人都可以看到它的作用和作用方式。请不要犹豫,报告任何问题,提出改进建议,甚至提交一些代码
8.2. jsonwebtoken.io and java.jsonwebtoken.io
8.2. jsonwebtoken.io 和 java.jsonwebtoken.io
jsonwebtoken.io is a developer tool we created to make it easy to decode JWTs. Simply paste an existing JWT into the appropriate field to decode its header, payload, and signature. jsonwebtoken.io is powered by nJWT, the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. You can also see code generated for a variety of languages at this website. The website itself is open-source and can be found here.
jsonwebtoken.io是我们创建的一个开发人员工具,旨在使 JWT 的解码变得简单。只需将现有的JWT粘贴到相应的字段中,就可以解码其标题、有效载荷和签名。jsonwebtoken.io由nJWT提供支持,这是面向Node.js开发人员的最简洁的免费开源(Apache许可,版本2.0)JWT库。您还可以在这个网站上看到为各种语言生成的代码。该网站本身是开源的,可以在这里找到。
java.jsonwebtoken.io is specifically for the JJWT library. You can alter the headers and payload in the upper right box, see the JWT generated by JJWT in the upper left box, and see a sample of the builder and parser Java code in the lower boxes. The website itself is open source and can be found here.
java.jsonwebtoken.io是专门针对JJWT库的。您可以在右上角的方框中改变头文件和有效载荷,在左上角的方框中看到由JJWT生成的JWT,并在下方的方框中看到生成器和解析器的Java代码样本。该网站本身是开源的,可以在这里找到。
8.3. JWT Inspector
8.3.JWT检查器
The new kid on the block, JWT Inspector is an open source Chrome extension that allows developers to inspect and debug JWTs directly in-browser. The JWT Inspector will discover JWTs on your site (in cookies, local/session storage, and headers) and make them easily accessible through your navigation bar and DevTools panel.
JWT Inspector是一个开源的Chrome扩展,允许开发人员直接在浏览器中检查和调试JWTs。JWT检查器将发现您网站上的JWT(在cookie、本地/会话存储和头文件中),并使它们能够通过您的导航栏和DevTools面板轻松访问。
9. JWT This Down!
9.JWT This Down!
JWTs add some intelligence to ordinary tokens. The ability to cryptographically sign and verify, build in expiration times and encode other information into JWTs sets the stage for truly stateless session management. This has a big impact on the ability to scale applications.
JWTs为普通令牌增加了一些智能。加密签名和验证、建立过期时间以及将其他信息编码到JWT中的能力为真正的无状态会话管理创造了条件。这对扩展应用程序的能力有很大影响。
At Stormpath, we use JWTs for OAuth2 tokens, CSRF tokens and assertions between microservices, among other usages.
在Stormpath,我们将JWTs用于OAuth2令牌、CSRF令牌和微服务之间的断言,以及其他用途。
Once you start using JWTs, you may never go back to the dumb tokens of the past. Have any questions? Hit me up at @afitnerd on twitter.
一旦你开始使用JWTs,你可能永远不会再回到过去的愚蠢的令牌。有任何问题吗?请在 twitter 上联系我:@afitnerd>。