委托JSON Web Tokens验证以提高灵活性的教程

113 阅读6分钟

委托JWT验证以获得更大的灵活性

使用回调来验证JSON网络令牌的Java解耦解决方案,从而促进解耦和灵活性。

在我看来,到目前为止,所有已经创建的、正在开发的和将要开发的软件应用程序的目的应该主要是为了使人类的日常活动更容易完成。人类是最有价值的创造物,而软件应用程序是伟大的工具,至少可以被他们使用。

如今,几乎每个软件产品 都会与至少一个其他同行的软件产品交换数据 ,这导致大量的数据在它们之间流动。通常,从一个产品到另一个产品的请求需要通过一系列的前提条件,然后才被认为是可接受的和可信赖的。

本文的目的是展示一个简单、灵活而又高效的解耦解决方案,用于验证此类前提条件。

设置舞台

让我们考虑下一个简单而普遍的用例:

  • 服务提供者和客户是两个交换数据的应用程序。
  • 客户端调用服务提供者。
  • 调用的操作只有在服务提供者识别了客户后才会执行。
  • 客户端的识别是通过请求中包含的令牌完成的,并由服务提供者验证。

作为这篇文章的一部分,我们建立了一个小型的Java项目,在做这个项目的同时,我们解释了令牌验证策略。

由于JSON Web令牌(JWT)现在被广泛使用,特别是当产品需要识别其他产品时,JWT验证被选为具体实现。根据RFC7519,JWT是一个JSON消息的紧凑、编码、URL安全的字符串表示。

简而言之,JWT有三个部分--头、有效载荷和签名。

编码后,它是一个由三个部分组成的字符串,用点分开:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJoY2QiLCJpc3MiOiJpc3N1ZXIiLCJhdWQiOiJhdWRpZW5jZSIsImV4cCI6MTY1MDU0OTg1OH0.rbs6NqNw9KZ4IGuCOjdPpdJqMswTXHn7oNADCzlQHL8

解码后,它是JSON格式,因此,更容易阅读。

头部 - 算法和类型

JSON

{
  "alg": "HS256",
  "typ": "JWT"
}

有效载荷--数据(索赔)

JSON

{
  "sub": "hcd",
  "iss": "issuer",
  "aud": "audience",
  "exp": 1650549858
}

Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  the-256-bit-secret
)

这些信息足以让我们对JWTs有一个了解;让我们开始开发吧。

最初的实施

该示例项目是用Java 17Maven构建的。依赖的东西非常少。

  • io.jsonwebtoken / jjwt - 用于JWT签名和验证

XML

<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>

要探索其他可用的库,请查看jwt.io/libraries?l…

XML

<dependency>
	<groupId>org.junit.jupiter</groupId>
	<artifactId>junit-jupiter-engine</artifactId>
	<version>5.8.2</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-junit-jupiter</artifactId>
	<version>4.5.1</version>
	<scope>test</scope>
</dependency>

JWT的生成和验证是通过以下接口实现的:

Java

public interface JwtManager {

    String generate(String sub, String iss, String aud);

    boolean isValid(String jwt, String iss, String aud);
}

前一个方法使用提供的参数(subject、issuer和audience)来创建和签署一个有效的JWT。后者使用提供的发行者和听众检查JWT是否有效。

我们的目标是创建一个实现并使下面的测试通过:

爪哇

class JwtManagerTest {

    private String iss;
    private String aud;
    private String jwt;
    private JwtManager jwtManager;

    @BeforeEach
    void setUp() {
        jwtManager = new JwtManagerImpl();

        iss = "issuer";
        aud = "audience";
        jwt = jwtManager.generate("hcd", iss, aud);
        Assertions.assertNotNull(jwt);
    }

    @Test
    void isValid_coupled() {
        final boolean valid = jwtManager.isValid(jwt, iss, aud);
        Assertions.assertTrue(valid);
    }
}

通过利用Jwts生成器,设置了subiss 和aud ,令牌被配置为1分钟后过期,此外,它还使用服务提供商的秘钥进行签名。

爪哇

public String generate(String sub, String iss, String aud) {
	final Date exp = new Date(System.currentTimeMillis() + 60_000);

	return Jwts.builder()
		.setSubject(sub)
		.setIssuer(iss)
		.setAudience(aud)
		.setExpiration(exp)
		.signWith(SignatureAlgorithm.HS256, "s1e2c3r4e5t6k7e8y9")
		.compact();
}

在另一个方向,使用相同的秘钥解析令牌,如果它还没有过期,则提取有效载荷要求。

爪哇

public boolean isValid(String jwt, String iss, String aud) {
	Claims body;
	try {
		body = Jwts.parser()
				.setSigningKey("s1e2c3r4e5t6k7e8y9")
				.parseClaimsJws(jwt)
				.getBody();
	} catch (JwtException e) {
		return false;
	}

	return iss.equals(body.getIssuer()) &&
			aud.equals(body.getAudience());
}

这是直截了当的。然而,除了标准(强制性)的令牌验证外,还做了一个自定义的假设。

"如果发行者和听众符合特定的值,那么有效的令牌是可以接受的。"

基本上,这就是本文的情节--如何尽可能灵活地实现对有效令牌的自定义验证。

如果我们运行测试,它通过了,实现是正确的,但不幸的是,不够灵活。

在某些时候,验证客户请求的服务提供者会改变之前的假设。这显然会影响到 isValid() 方法,其实现应该被改变。

最终实现

如果服务提供者对这些前提条件做出改变时,令牌验证的标准部分仍然保持原样,那就更好了。然后,代码应足够灵活,允许尽可能晚地决定自定义验证假设。为了适应这一点,代码需要被重构。

已经说过的,它被封闭在下一个接口中(甚至更好,@FunctionalInterface )。

爪哇

@FunctionalInterface
public interface ValidationStrategy {

    boolean isValid(Claims body);
}

策略中实现,isValid() 方法中的最后两行被移到新实现的策略中。此外,我们可以假设这是服务提供者的默认验证策略。

爪哇

public class DefaultValidationStrategy implements ValidationStrategy {

    private final String iss;
    private final String aud;

    public DefaultValidationStrategy(String iss, String aud) {
        this.iss = iss;
        this.aud = aud;
    }

    @Override
    public boolean isValid(Claims body) {
        return iss.equals(body.getIssuer()) &&
                aud.equals(body.getAudience());
    }
}

以前的方法首先被废弃,很快被下面的新实现所取代:

爪哇

public interface JwtManager {

    String generate(String sub, String iss, String aud);

    /**
     * @deprecated in favor of {@link #isValid(String, ValidationStrategy)}
     */
    @Deprecated(forRemoval = true)
    boolean isValid(String jwt, String iss, String aud);

    boolean isValid(String jwt, ValidationStrategy strategy);
}

基本上,新方法委托给了ValidationStrategy 回调。委托(在编程中)的意思正是如此;一个实体将某些东西传递给另一个实体。

爪哇

public boolean isValid(String jwt, ValidationStrategy strategy) {
	Claims body;
	try {
		body = Jwts.parser()
				.setSigningKey(SECRET)
				.parseClaimsJws(jwt)
				.getBody();
	} catch (JwtException e) {
		return false;
	}

	return strategy.isValid(body);
}

在使用中,验证是按照下面的单元测试进行的:

爪哇

@Test
void isValid_looselyCoupled_defaultStrategy() {
	final boolean valid = jwtManager.isValid(jwt,
			new DefaultValidationStrategy(iss, aud));
	Assertions.assertTrue(valid);
} 

通过这些修改,代码足够灵活,以适应验证策略的潜在变化。例如,如果服务提供者决定只检查发行者,这可以实现,而不需要修改处理JWT标准部分的代码。

爪哇

@Test
void isValid_looselyCoupled_customStrategy() {
	final boolean valid = jwtManager.isValid(jwt,
			body -> iss.equals(body.getIssuer()));
	Assertions.assertTrue(valid);
}

如果我们看一下前面的单元测试,就会发现使用lambda来传递ValidationStrategy ,是多么的方便。另外,我想从一开始就把ValidationStrategy 改为@FunctionalInterface 的原因也很清楚了。

在这篇文章中,我们实现了一个验证JSON Web Tokens的解耦方案。这个解决方案使用回调,从而促进了解耦和灵活性。