1919import static java .nio .charset .StandardCharsets .UTF_8 ;
2020import static org .apache .solr .client .solrj .SolrRequest .METHOD .GET ;
2121
22+ import com .github .benmanes .caffeine .cache .Caffeine ;
23+ import com .github .benmanes .caffeine .cache .LoadingCache ;
2224import com .google .common .annotations .VisibleForTesting ;
2325import jakarta .servlet .FilterChain ;
2426import jakarta .servlet .http .HttpServletRequest ;
3739import java .util .Optional ;
3840import java .util .Set ;
3941import java .util .concurrent .ConcurrentHashMap ;
42+ import java .util .concurrent .TimeUnit ;
4043import org .apache .http .HttpEntity ;
4144import org .apache .http .HttpException ;
4245import org .apache .http .HttpHeaders ;
@@ -94,7 +97,10 @@ public static void withServerIdentity(final boolean enabled) {
9497 private final Map <String , PublicKey > keyCache = new ConcurrentHashMap <>();
9598 private final PublicKeyHandler publicKeyHandler ;
9699 private final CoreContainer cores ;
97- private static final int MAX_VALIDITY = Integer .getInteger ("pkiauth.ttl" , 5000 );
100+ private final LoadingCache <String , PKIHeaderData > validatedHeaderCache ;
101+ private final LoadingCache <String , String > generatedV1TokenCache ;
102+ private final LoadingCache <String , String > generatedV2TokenCache ;
103+ private static final int MAX_VALIDITY = Integer .getInteger ("pkiauth.ttl" , 10000 );
98104 private final String myNodeName ;
99105 private final HttpHeaderClientInterceptor interceptor = new HttpHeaderClientInterceptor ();
100106 private boolean interceptorRegistered = false ;
@@ -114,6 +120,38 @@ public PKIAuthenticationPlugin(
114120 this .cores = cores ;
115121 myNodeName = nodeName ;
116122
123+ // Don't expire after read, because there is no reason to add the overhead of updating expiry
124+ // information after each read. The expiration time here doesn't matter too much, because we
125+ // still check the PKI Token TTL after fetching from the cache. We just want to make sure cache
126+ // entries are cleaned up regularly.
127+ validatedHeaderCache =
128+ Caffeine .newBuilder ()
129+ .maximumSize (1000 )
130+ .expireAfterWrite (MAX_VALIDITY , TimeUnit .MILLISECONDS )
131+ .build (this ::decipherHeaderV2 );
132+ // We must expire much earlier than the max validity, because these cached Auth tokens still
133+ // need to be sent to the server, which will validate the TTL. If we expire at maxValidity, the
134+ // TTL check will always fail before the cache entry is expired.
135+ long expireAfterTime = MAX_VALIDITY / 4 ;
136+ // Refreshing is done asynchronously, so we want to do it before expiration. This means that
137+ // requests are not synchronously blocked when generating new Auth tokens. However, the refresh
138+ // will only happen when the cached header is requested. Therefore, we want to give a long-ish
139+ // runway for requests to come in to trigger an asynchronous-refresh before expiry causes a
140+ // synchronous-refresh.
141+ long shouldRefreshTime = Math .max (1 , expireAfterTime / 2 );
142+ generatedV1TokenCache =
143+ Caffeine .newBuilder ()
144+ .maximumSize (100 )
145+ .refreshAfterWrite (shouldRefreshTime , TimeUnit .MILLISECONDS )
146+ .expireAfterWrite (expireAfterTime , TimeUnit .MILLISECONDS )
147+ .build (this ::generateToken );
148+ generatedV2TokenCache =
149+ Caffeine .newBuilder ()
150+ .maximumSize (100 )
151+ .refreshAfterWrite (shouldRefreshTime , TimeUnit .MILLISECONDS )
152+ .expireAfterWrite (expireAfterTime , TimeUnit .MILLISECONDS )
153+ .build (this ::generateTokenV2 );
154+
117155 Set <String > knownPkiVersions = Set .of ("v1" , "v2" );
118156 // We always accept v2 even if it is not specified
119157 String [] versions = System .getProperty (ACCEPT_VERSIONS , "v2" ).split ("," );
@@ -155,7 +193,7 @@ public boolean doAuthenticate(
155193 return sendError (response , true , "Could not parse node name from SolrAuthV2 header." );
156194 }
157195
158- headerData = decipherHeaderV2 ( headerV2 , headerV2 . substring ( 0 , nodeNameEnd ) );
196+ headerData = validatedHeaderCache . get ( headerV2 );
159197 } else if (headerV1 != null && acceptPkiV1 ) {
160198 List <String > authInfo = StrUtils .splitWS (headerV1 , false );
161199 if (authInfo .size () != 2 ) {
@@ -223,7 +261,8 @@ private PublicKey getOrFetchPublicKey(String nodeName) {
223261 return key ;
224262 }
225263
226- private PKIHeaderData decipherHeaderV2 (String header , String nodeName ) {
264+ private PKIHeaderData decipherHeaderV2 (String header ) {
265+ String nodeName = header .substring (0 , header .indexOf (' ' ));
227266 PublicKey key = getOrFetchPublicKey (nodeName );
228267
229268 int sigStart = header .lastIndexOf (' ' );
@@ -414,11 +453,11 @@ public void onBegin(Request request) {
414453 final Optional <String > preFetchedUser = getUserFromJettyRequest (request );
415454 if ("v1" .equals (System .getProperty (SEND_VERSION ))) {
416455 preFetchedUser
417- .map (PKIAuthenticationPlugin . this :: generateToken )
456+ .map (generatedV1TokenCache :: get )
418457 .ifPresent (token -> request .headers (httpFields -> httpFields .add (HEADER , token )));
419458 } else {
420459 preFetchedUser
421- .map (PKIAuthenticationPlugin . this :: generateTokenV2 )
460+ .map (generatedV2TokenCache :: get )
422461 .ifPresent (
423462 token -> request .headers (httpFields -> httpFields .add (HEADER_V2 , token )));
424463 }
@@ -520,10 +559,12 @@ private String generateTokenV2(String user) {
520559
521560 void setHeader (HttpRequest httpRequest ) {
522561 if ("v1" .equals (System .getProperty (SEND_VERSION ))) {
523- getUser ().map (this ::generateToken ).ifPresent (token -> httpRequest .setHeader (HEADER , token ));
562+ getUser ()
563+ .map (generatedV1TokenCache ::get )
564+ .ifPresent (token -> httpRequest .setHeader (HEADER , token ));
524565 } else {
525566 getUser ()
526- .map (this :: generateTokenV2 )
567+ .map (generatedV2TokenCache :: get )
527568 .ifPresent (token -> httpRequest .setHeader (HEADER_V2 , token ));
528569 }
529570 }
0 commit comments