读书笔记《building-restful-web-services-with-spring-5-second-edition》Spring Security和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 是一个强大的authentication 和授权框架,它将帮助我们提供一个安全的应用程序。通过使用 Spring Security,我们可以保持所有 REST API 的安全,并且只能通过经过身份验证和授权的调用来访问。
Spring Security 可以应用于许多 形式,包括使用JWT 等强大库的XML 配置。由于大多数公司在其安全性中使用 JWT,我们将更多地关注基于 JWT 的安全性,而不是简单的 Spring Security,这可以在 XML 中配置。
JWT 令牌是 URL 安全和网络浏览器 -兼容 特别适用于 单点登录-On (SSO) 上下文。 JWT 分为三个部分:
- Header
- Payload
- Signature
标头部分决定应该使用哪种算法来生成令牌。在进行身份验证时,客户端必须保存服务器返回的 JWT。与传统的会话创建方法不同,此过程不需要在客户端存储任何 cookie。 JWT 身份验证是无状态的,因为客户端状态永远不会保存在服务器上。
要在我们的应用程序中使用 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>
为了创建一个令牌,我们在 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 算法。
我们在这个类中硬编码了密钥。我们还可以将密钥存储在 application.properties
文件中。然而,为了简化流程,我们对其进行了硬编码:
private static final String secretKey= "4C8kum4LxyKWYLM78sKdXrzbBjDCFyfX";
我们将字符串键转换为字节数组,然后将其传递给 Java 类 SecretKeySpec
,以获取 signingKey
。此密钥将在令牌生成器中使用。此外,在创建签名密钥时,我们使用 JCA,即我们的签名算法的名称。
我们使用 JwtBuilder
类来创建令牌并设置 expiration 时间为了它。以下代码定义了令牌创建和到期时间设置选项:
JwtBuilder builder = Jwts.builder() .setSubject(subject) .signWith(signatureAlgorithm, signingKey); long nowMillis = System.currentTimeMillis(); builder.setExpiration(new Date(nowMillis + ttlMillis));
最后,我们必须在 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; }
我们可以通过在浏览器或任何 REST 客户端中调用 API 来测试令牌。通过调用这个API,我们可以创建一个token。此令牌将用于类似用户身份验证的目的。
创建令牌的示例 API 如下:
http://localhost:8080/security/generate/token?subject=one
这里我们使用 one
作为主语。我们可以在以下结果中看到令牌。这是为我们传递给 API 的所有主题生成令牌的方式:
{ result: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJvbmUiLCJleHAiOjE1MDk5MzY2ODF9.GknKcywiI-G4-R2bRmBOsjomujP0MxZqdawrB8TO3P4" }
到目前为止,我们已经创建了一个 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; }
之前,我们创建了获取令牌的代码。在这里,我们将通过调用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" }