View Javadoc
1   /*
2    * Copyright 2020 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.security.authentication;
18  
19  import java.time.Duration;
20  import java.util.Objects;
21  import java.util.Optional;
22  import java.util.Set;
23  import java.util.Timer;
24  import java.util.TimerTask;
25  import java.util.function.BiFunction;
26  import javax.validation.constraints.NotNull;
27  import lombok.Setter;
28  import lombok.extern.slf4j.Slf4j;
29  import org.springframework.beans.factory.DisposableBean;
30  import org.springframework.cache.Cache;
31  import org.springframework.cache.concurrent.ConcurrentMapCache;
32  import org.springframework.util.StringUtils;
33  
34  /**
35   * The access token cache implementation.
36   *
37   * @author Christian Bremer
38   */
39  @Slf4j
40  public class AccessTokenCacheImpl implements AccessTokenCache, DisposableBean {
41  
42    private Timer internalCacheTimer;
43  
44    private final Cache cache;
45  
46    private final Duration expirationTimeThreshold;
47  
48    private final String keyPrefix;
49  
50    @Setter
51    @NotNull
52    private BiFunction<String, Duration, Boolean> expiredBiFn = AccessTokenCache::isExpired;
53  
54    /**
55     * Instantiates a new access token cache.
56     */
57    @SuppressWarnings("unused")
58    public AccessTokenCacheImpl() {
59      this(null, null);
60    }
61  
62    /**
63     * Instantiates a new access token cache.
64     *
65     * @param expirationTimeThreshold the expiration time threshold
66     * @param keyPrefix the key prefix
67     */
68    public AccessTokenCacheImpl(Duration expirationTimeThreshold, String keyPrefix) {
69      this(null, expirationTimeThreshold, keyPrefix);
70    }
71  
72    /**
73     * Instantiates a new access token cache.
74     *
75     * @param cache the external cache
76     */
77    @SuppressWarnings("unused")
78    public AccessTokenCacheImpl(Cache cache) {
79      this(cache, null, null);
80    }
81  
82    /**
83     * Instantiates a new access token cache.
84     *
85     * @param cache the cache
86     * @param expirationTimeThreshold the expiration time threshold
87     * @param keyPrefix the key prefix
88     */
89    public AccessTokenCacheImpl(
90        Cache cache,
91        Duration expirationTimeThreshold,
92        String keyPrefix) {
93      if (cache != null) {
94        log.info("Creating access token cache with given cache.");
95        this.cache = cache;
96      } else {
97        log.info("Creating access token cache with internal in memory cache.");
98        this.cache = createInternalCache();
99      }
100     this.expirationTimeThreshold = Objects
101         .requireNonNullElseGet(expirationTimeThreshold, () -> Duration.ofSeconds(20L));
102     this.keyPrefix = keyPrefix;
103   }
104 
105   private ConcurrentMapCache createInternalCache() {
106     final long period = 1000L * 60L * 30L;
107     ConcurrentMapCache internalCache = new ConcurrentMapCache(CACHE_NAME);
108     internalCacheTimer = new Timer();
109     internalCacheTimer.schedule(new TimerTask() {
110       @Override
111       public void run() {
112         Set<Object> keys = internalCache.getNativeCache().keySet();
113         log.trace("Removing obsolete jwt entries from internal cache (sze = {}).", keys.size());
114         keys.forEach(key -> findAccessToken(String.valueOf(key))
115             .filter(token -> expiredBiFn.apply(token, expirationTimeThreshold))
116             .ifPresent(token -> internalCache.evict(key)));
117       }
118     }, period, period);
119     return internalCache;
120   }
121 
122   private String addKeyPrefix(String givenKey) {
123     if (StringUtils.hasText(keyPrefix) && !givenKey.startsWith(keyPrefix)) {
124       return keyPrefix + givenKey;
125     }
126     return givenKey;
127   }
128 
129   @Override
130   public Optional<String> findAccessToken(String key) {
131     try {
132       return Optional.ofNullable(cache.get(addKeyPrefix(key), String.class))
133           .filter(token -> !expiredBiFn.apply(token, expirationTimeThreshold));
134 
135     } catch (RuntimeException e) {
136       log.error("Getting access token from cache failed.", e);
137       return Optional.empty();
138     }
139   }
140 
141   @Override
142   public void putAccessToken(String key, String accessToken) {
143     try {
144       cache.put(addKeyPrefix(key), accessToken);
145 
146     } catch (RuntimeException e) {
147       log.error("Putting access token into the cache failed.", e);
148     }
149   }
150 
151   @Override
152   public void destroy() {
153     if (internalCacheTimer != null) {
154       internalCacheTimer.cancel();
155     }
156   }
157 
158   @Override
159   public String toString() {
160     return "AccessTokenCacheImpl {cache = "
161         + (internalCacheTimer != null ? "INTERNAL" : "EXTERNAL")
162         + ", keyPrefix = " + keyPrefix
163         + ", expirationTimeThreshold (in secs) = " + expirationTimeThreshold.toSeconds()
164         + '}';
165   }
166 }