如何在 spring-security 5.2 中增加 RemoteJWKSet 缓存 TTL

Posted

技术标签:

【中文标题】如何在 spring-security 5.2 中增加 RemoteJWKSet 缓存 TTL【英文标题】:How to increase RemoteJWKSet cache TTL in spring-security 5.2 【发布时间】:2020-06-10 01:49:26 【问题描述】:

我们正在使用 spring-security 5.2 通过 JWT 验证来保护我们的 REST API。

使用spring:security:oauth2:resourceserver:jwt:jwk-set-uri 属性,我们指示远程 JWKS 端点,它 翻译成 Spring 基于这个 URI 创建一个 NimbusJwtDecoder。 再往下,创建一个 RemoteJWKSet 对象,该对象缓存对 JWKS 端点的调用,默认 TTL 为 5 分钟。

有没有办法增加这个 TTL 以最小化远程调用? 也许在某个具有不同 TTL 的地方注入一个新的 DefaultJWKSetCache 实例? 将其尽可能长时间地保存在缓存中似乎是安全的,因为当我们收到带有未知孩子的令牌时,将恢复对 JWKS 端点的调用以更新密钥集。

获取密钥的调用栈如下

JwtAuthenticationProvider
  public Authentication authenticate(Authentication authentication)
    ...
      jwt = this.jwtDecoder.decode(bearer.getToken())
    ...

o.s.security.oauth2.jwt.NimbusJwtDecoder
    public Jwt decode(String token)
    ...
      Jwt createdJwt = createJwt(token, jwt);
    ...

    private Jwt createJwt(String token, JWT parsedJwt)
    ...
      JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
    ....

DefaultJWTProcessor
      public JWTClaimsSet process(final JWT jwt, final C context)
        ...
          if (jwt instanceof SignedJWT) 
                return process((SignedJWT)jwt, context);
                
        ...

      public JWTClaimsSet process(final SignedJWT signedJWT, final C context)
            ...
              List<? extends Key> keyCandidates = selectKeys(signedJWT.getHeader(), claimsSet, context);
          ...

      private List<? extends Key> selectKeys(final JWSHeader header, final JWTClaimsSet claimsSet, final C context)
        ....
          if (getJWSKeySelector() != null) 
                 return getJWSKeySelector().selectJWSKeys(header, context);
                       
        ....  


JWSVerificationKeySelector
  public List<Key> selectJWSKeys(final JWSHeader jwsHeader, final C context)
    ...
      List<JWK> jwkMatches = getJWKSource().get(new JWKSelector(jwkMatcher), context);
    ...

RemoteJWKSet
  public List<JWK> get(final JWKSelector jwkSelector, final C context)
  ...
    JWKSet jwkSet = jwkSetCache.get();
        if (jwkSet == null) 
            jwkSet = updateJWKSetFromURL();
        
  ...


DefaultJWKSetCache  
  public JWKSet get() 

    if (isExpired()) 
      jwkSet = null; // clear
    

    return jwkSet;
  

安全依赖:

+- org.springframework.boot:spring-boot-starter-security:jar:2.2.4.RELEASE:compile
|  +- org.springframework.security:spring-security-config:jar:5.2.1.RELEASE:compile
|  \- org.springframework.security:spring-security-web:jar:5.2.1.RELEASE:compile
+- org.springframework.security:spring-security-oauth2-jose:jar:5.2.2.RELEASE:compile
|  +- org.springframework.security:spring-security-core:jar:5.2.1.RELEASE:compile
|  \- org.springframework.security:spring-security-oauth2-core:jar:5.2.1.RELEASE:compile
+- com.nimbusds:nimbus-jose-jwt:jar:8.8:compile
|  +- com.github.stephenc.jcip:jcip-annotations:jar:1.0-1:compile
|  \- net.minidev:json-smart:jar:2.3:compile (version selected from constraint [1.3.1,2.3])
|     \- net.minidev:accessors-smart:jar:1.2:compile
|        \- org.ow2.asm:asm:jar:5.0.4:compile
+- org.springframework.security:spring-security-oauth2-resource-server:jar:5.2.1.RELEASE:compile

【问题讨论】:

这个版本的源码在Github上吗?没找到... nimbus-jose-jwt 的来源在 bitbucket bitbucket.org/connect2id/nimbus-jose-jwt/src/master/src/main/… 我还用依赖版本更新了帖子。 嘿!我其实也有同样的问题,你找到解决方法了吗? 长时间缓存 JWK 集不是一个好主意。在泄露 JWK 的情况下,撤销 JWK 的唯一方法是将其从已发布的 JWK 集中删除,而较长的缓存过期时间意味着您的应用程序需要很长时间才能注意到密钥已被撤销。 【参考方案1】:

我最终做了以下事情:

    @Bean
    public JwtDecoder jwtDecoder() 
        JWSKeySelector<SecurityContext> jwsKeySelector = null;
        try 
            URL jwksUrl = new URL("https://localhost/.well-known/openid-configuration/jwks");
            long cacheLifespan = 500;
            long refreshTime = 400;
            JWKSetCache jwkSetCache = new DefaultJWKSetCache(cacheLifespan, refreshTime, TimeUnit.MINUTES);
            RemoteJWKSet<SecurityContext> jwkSet = new RemoteJWKSet<>(jwksUrl,null,jwkSetCache);
            jwsKeySelector = JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(jwkSet);
        
        catch (KeySourceException e) 
            e.printStackTrace();
        
        catch (MalformedURLException e) 
            e.printStackTrace();
        

        DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        jwtProcessor.setJWSKeySelector(jwsKeySelector);

        return new NimbusJwtDecoder(jwtProcessor);
    

【讨论】:

你能对此发表评论吗 - ***.com/questions/70049215/… 有没有办法动态创建这个 JwtDecoder bean?我有多个租户(发行人),如果我在每个请求中检查它们,它们就不会被缓存吗?【参考方案2】:

看起来我参加聚会有点晚了,但我是为 5.4 版本实现此功能的人,现在您可以使用 Spring Cache 对其进行配置:

var jwkSetCache = new ConcurrentMapCache("jwkSetCache", CacheBuilder.newBuilder()
    // can set the value here or better populate from properties
    .expireAfterWrite(Duration.ofMinutes(30))
    .build().asMap(), false);
var decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
    .restOperations(restOperations)
    .cache(jwkSetCache)
    .build();

【讨论】:

你能对此发表评论吗 - ***.com/questions/70049215/…

以上是关于如何在 spring-security 5.2 中增加 RemoteJWKSet 缓存 TTL的主要内容,如果未能解决你的问题,请参考以下文章

如何在spring-security中切换角色

如何在spring-security的SecurityContext中存储自定义信息?

如何禁用 spring-security 登录屏幕?

如何根据spring-security用户权限生成内容

Facebook 登录后 (javascript sdk) - 如何在 Spring-Security 中创建用户会话?

如何使用 spring-security 和 jQuery 处理过期会话?