vlambda博客
学习文章列表

读书笔记《building-restful-web-services-with-spring-5-second-edition》Spring Security和JWT(JSON Web Token)

Chapter 6. Spring Security and JWT (JSON Web Token)

在本章中,我们将对 Spring Security 有一个简单的了解,我们还将讨论 JSON Web Token ( JWT)以及如何在我们的 Web 服务调用中使用 JWT。这 还包括JWT 创建。

在本章中,我们将介绍以下内容:

  • Spring Security
  • JSON Web Token (JWT)
  • How to generate JWTs in web services
  • How to access and retrieve information from JWTs in a web service
  • How to restrict web service calls by adding JWT security

Spring Security


Spring Security 是一个强大的authentication 和授权框架,它将帮助我们提供一个安全的应用程序。通过使用 Spring Security,我们可以保持所有 REST API 的安全,并且只能通过经过身份验证和授权的调用来访问。

Authentication and authorization

让我们看一个例子来解释这一点。假设你有一个有很多书的图书馆。身份验证将提供进入库的密钥;但是,授权将允许您拿书。没有钥匙,你甚至不能进入图书馆。即使您有图书馆的钥匙,您只被允许拿几本书。

JSON Web Token (JWT)

Spring Security 可以应用于许多 形式,包括使用JWT 等强大库的XML 配置。由于大多数公司在其安全性中使用 JWT,我们更多地关注基于 JWT 的安全性,而不是简单的 Spring Security,这可以在 XML 中配置。

JWT 令牌是 URL 安全和网络浏览器 -兼容 特别适用于 单点登录-On (SSO) 上下文。 JWT 分为三个部分:

  • Header
  • Payload
  • Signature

标头部分决定应该使用哪种算法来生成令牌。在进行身份验证时,客户端必须保存服务器返回的 JWT。与传统的会话创建方法不同,此过程不需要在客户端存储任何 cookie。 JWT 身份验证是无状态的,因为客户端状态永远不会保存在服务器上。

JWT dependency

要在我们的应用程序中使用 JWT,我们可能需要使用 Maven 依赖项。 pom.xml 文件中应添加以下依赖项。您可以从以下位置获取 Maven 依赖项:https://mvnrepository.com/artifact/javax.xml.bind

我们在应用程序中使用了版本 2.3.0 的 Maven 依赖项:

<dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.0</version>
</dependency>

Note

由于 Java 9 的包中不包含 DataTypeConverter,我们需要添加前面的配置以使用 DataTypeConverter。我们将在下一节介绍 DataTypeConverter

Creating a JWT token

为了创建一个令牌,我们在 abstract 方法中添加了一个名为 createToken我们的 SecurityService 接口。这个接口将告诉实现类它必须为 createToken 创建一个完整的方法。在 createToken 方法中,我们将只使用主题和到期时间,因为这两个选项在创建令牌时很重要。

首先,我们将在 SecurityService 接口中创建一个抽象方法。具体类(实现 SecurityService 接口的人)必须在他们的类中实现该方法:

public interface SecurityService {
  String createToken(String subject, long ttlMillis);    
 // other methods  
}

在前面的代码中,我们在接口中定义了创建token的方法。

SecurityServiceImpl是应用业务逻辑实现SecurityService接口抽象方法的具体类。以下代码将解释如何使用主题和到期时间创建 JWT:

private static final String secretKey= "4C8kum4LxyKWYLM78sKdXrzbBjDCFyfX";
@Override
public String createToken(String subject, long ttlMillis) {    
    if (ttlMillis <= 0) {
      throw new RuntimeException("Expiry time must be greater than Zero :["+ttlMillis+"] ");
    }    
    // The JWT signature algorithm we will be using to sign the token
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;   
    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secretKey);
    Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
    JwtBuilder builder = Jwts.builder()
        .setSubject(subject) 
        .signWith(signatureAlgorithm, signingKey);
    long nowMillis = System.currentTimeMillis();    
    builder.setExpiration(new Date(nowMillis + ttlMillis)); 
    return builder.compact();
}

前面的代码为主题创建了令牌。在这里,我们对密钥 "4C8kum4LxyKWYLM78sKdXrzbBjDCFyfX" 进行了硬编码,以简化令牌创建过程。如果需要,我们可以将密钥保存在属性文件中,以避免 Java 代码中的硬代码。

首先,我们验证时​​间是否大于零。如果没有,我们立即抛出异常。我们正在使用大多数应用程序中使用的 SHA-256 算法。

Note

安全哈希算法 (SHA) 是一种加密哈希函数。加密散列是数据文件的文本形式。 SHA-256 算法生成一个几乎唯一的、固定大小的 256 位散列。 SHA-256 是更可靠的哈希函数之一。

我们在这个类中硬编码了密钥。我们还可以将密钥存储在 application.properties 文件中。然而,为了简化流程,我们对其进行了硬编码:

private static final String secretKey= "4C8kum4LxyKWYLM78sKdXrzbBjDCFyfX";

我们将字符串键转换为字节数组,然后将其传递给 Java 类 SecretKeySpec,以获取 signingKey。此密钥将在令牌生成器中使用。此外,在创建签名密钥时,我们使用 JCA,即我们的签名算法的名称。

Note

Java Cryptography Architecture (JCA) 是由 Java 引入以支持现代密码学技术。

我们使用 JwtBuilder 类来创建令牌并设置 expiration 时间为了它。以下代码定义了令牌创建和到期时间设置选项:

JwtBuilder builder = Jwts.builder()
        .setSubject(subject) 
        .signWith(signatureAlgorithm, signingKey);
long nowMillis = System.currentTimeMillis(); 
builder.setExpiration(new Date(nowMillis + ttlMillis)); 

Note

在调用此方法时,我们必须以毫秒为单位传递时间,因为 setExpiration 只需要毫秒。

最后,我们必须在 HomeController 中调用 createToken 方法。在调用该方法之前,我们必须自动装配 SecurityService,如下所示:

@Autowired
SecurityService securityService;

createToken 调用编码如下。我们以主题为参数。为了简化流程,我们将到期时间硬编码为 2 * 1000 * 60(两分钟)。

HomeController.java

@Autowired
SecurityService securityService;
@ResponseBody
  @RequestMapping("/security/generate/token")
  public Map<String, Object> generateToken(@RequestParam(value="subject") String subject){    
    String token = securityService.createToken(subject, (2 * 1000 * 60));    
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("result", token);    
    return map;
  }

Generating a token

我们可以通过在浏览器或任何 REST 客户端中调用 API 来测试令牌。通过调用这个API,我们可以创建一个token。此令牌将用于类似用户身份验证的目的。

创建令牌的示例 API 如下:

http://localhost:8080/security/generate/token?subject=one

这里我们使用 one 作为主语。我们可以在以下结果中看到令牌。这是为我们传递给 API 的所有主题生成令牌的方式:

{
  result: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJvbmUiLCJleHAiOjE1MDk5MzY2ODF9.GknKcywiI-G4-R2bRmBOsjomujP0MxZqdawrB8TO3P4"
}

Note

JWT 是一个包含三个部分的字符串,每个部分用点 (.) 分隔。每个部分都是 base-64 编码的。第一部分是标题,它提供了有关用于签署 JWT 的算法的线索。第二部分是正文,最后一部分是签名。

Getting a subject from a JWT token

到目前为止,我们已经创建了一个 JWT 令牌。在这里,我们开始 解码令牌并从中获取主题。在以后的部分中,我们将讨论如何从令牌中解码和获取主题。

像往常一样,我们必须定义获取主题的方法。我们将在 SecurityService 中定义 getSubject 方法。

在这里,我们将在 SecurityService 接口中创建一个名为 getSubject 的抽象方法。稍后,我们将在具体类中实现此方法:

String getSubject(String token);

在我们的具体类中,我们将实现 getSubject 方法并将我们的代码添加到 SecurityServiceImpl 类中。我们可以使用以下代码从令牌中获取主题:

  @Override
  public String getSubject(String token) {     
    Claims claims = Jwts.parser()              .setSigningKey(DatatypeConverter.parseBase64Binary(secretKey))
             .parseClaimsJws(token).getBody();    
    return claims.getSubject();
  } 

在上述方法中,我们使用 Jwts.parser 来获取 claims。我们通过将密钥转换为二进制然后将其传递给解析器来设置签名密钥。一旦我们得到 Claims,我们就可以通过调用 getSubject 来简单地获取主题。

最后,我们可以调用控制器中的方法并传递生成的令牌以获取主题。您可以检查以下代码,其中控制器正在调用 getSubject 方法并在 HomeController.java 文件中返回主题:

  @ResponseBody
  @RequestMapping("/security/get/subject")
  public Map<String, Object> getSubject(@RequestParam(value="token") String token){    
    String subject = securityService.getSubject(token);    
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("result", subject);    
    return map;
  }

Getting a subject from a token

之前,我们创建了获取令牌的代码。在这里,我们通过调用get subject API来测试我们之前创建的方法。通过调用 REST API,我们将获得我们之前传递的主题。

示例 API:

http://localhost:8080/security/get/subject?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJvbmUiLCJleHAiOjE1MDk5MzY2ODF9.GknKcywiI-G4-R2bRmBOsjomujP0MxZqdawrB8TO3P4

由于我们在调用generateToken方法创建token时使用了one作为主体,所以我们会得到getSubject 方法中的">"one":

{
  result: "one"
}

Note

通常,我们将令牌附加在标头中;但是,为了避免复杂性,我们提供了结果。此外,我们已将令牌作为参数传递以获取主题。您可能不需要在实际应用程序中以相同的方式执行此操作。这仅用于演示目的。

Summary


在本章中,我们讨论了 Spring Security 和基于 JWT 令牌的安全性来获取和解码令牌。在以后的章节中,我们将讨论如何在 AOP 中使用令牌,并通过使用 JWT 令牌来限制 API 调用。