浏览代码

增加redis token manager

zhou-hao 5 年之前
父节点
当前提交
dac5d93f27

+ 7 - 0
hsweb-authorization/hsweb-authorization-api/pom.xml

@@ -24,6 +24,13 @@
             <optional>true</optional>
         </dependency>
 
+        <dependency>
+            <groupId>io.lettuce</groupId>
+            <artifactId>lettuce-core</artifactId>
+            <version>5.2.0.RELEASE</version>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>

+ 8 - 12
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java

@@ -30,13 +30,9 @@ import org.springframework.context.ApplicationEventPublisher;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
-import javax.validation.constraints.NotNull;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
 /**
  * 默认到用户令牌管理器,使用ConcurrentMap来存储令牌信息
@@ -46,7 +42,7 @@ import java.util.stream.Collectors;
  */
 public class DefaultUserTokenManager implements UserTokenManager {
 
-    protected final ConcurrentMap<String, SimpleUserToken> tokenStorage;
+    protected final ConcurrentMap<String, LocalUserToken> tokenStorage;
 
     protected final ConcurrentMap<String, Set<String>> userStorage;
 
@@ -61,11 +57,11 @@ public class DefaultUserTokenManager implements UserTokenManager {
 
     }
 
-    public DefaultUserTokenManager(ConcurrentMap<String, SimpleUserToken> tokenStorage) {
+    public DefaultUserTokenManager(ConcurrentMap<String, LocalUserToken> tokenStorage) {
         this(tokenStorage, new ConcurrentHashMap<>());
     }
 
-    public DefaultUserTokenManager(ConcurrentMap<String, SimpleUserToken> tokenStorage, ConcurrentMap<String, Set<String>> userStorage) {
+    public DefaultUserTokenManager(ConcurrentMap<String, LocalUserToken> tokenStorage, ConcurrentMap<String, Set<String>> userStorage) {
         this.tokenStorage = tokenStorage;
         this.userStorage = userStorage;
     }
@@ -183,7 +179,7 @@ public class DefaultUserTokenManager implements UserTokenManager {
         if (token == null) {
             return;
         }
-        SimpleUserToken tokenObject = tokenStorage.remove(token);
+        LocalUserToken tokenObject = tokenStorage.remove(token);
         if (tokenObject != null) {
             String userId = tokenObject.getUserId();
             if (removeUserToken) {
@@ -212,8 +208,8 @@ public class DefaultUserTokenManager implements UserTokenManager {
 
     public Mono<Void> changeTokenState(UserToken userToken, TokenState state) {
         if (null != userToken) {
-            SimpleUserToken token = ((SimpleUserToken) userToken);
-            SimpleUserToken copy = token.copy();
+            LocalUserToken token = ((LocalUserToken) userToken);
+            LocalUserToken copy = token.copy();
 
             token.setState(state);
             syncToken(userToken);
@@ -239,7 +235,7 @@ public class DefaultUserTokenManager implements UserTokenManager {
     public Mono<UserToken> signIn(String token, String type, String userId, long maxInactiveInterval) {
 
         return Mono.defer(() -> {
-            SimpleUserToken detail = new SimpleUserToken(userId, token);
+            LocalUserToken detail = new LocalUserToken(userId, token);
             detail.setType(type);
             detail.setMaxInactiveInterval(maxInactiveInterval);
             detail.setState(TokenState.normal);
@@ -278,7 +274,7 @@ public class DefaultUserTokenManager implements UserTokenManager {
 
     @Override
     public Mono<Void> touch(String token) {
-        SimpleUserToken userToken = tokenStorage.get(token);
+        LocalUserToken userToken = tokenStorage.get(token);
         if (null != userToken) {
             userToken.touch();
             syncToken(userToken);

+ 5 - 5
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleUserToken.java

@@ -8,7 +8,7 @@ import java.util.concurrent.atomic.AtomicLong;
  * @author zhouhao
  * @since 3.0
  */
-public class SimpleUserToken implements UserToken {
+public class LocalUserToken implements UserToken {
 
     private static final long serialVersionUID = 1L;
 
@@ -39,12 +39,12 @@ public class SimpleUserToken implements UserToken {
         this.maxInactiveInterval = maxInactiveInterval;
     }
 
-    public SimpleUserToken(String userId, String token) {
+    public LocalUserToken(String userId, String token) {
         this.userId = userId;
         this.token = token;
     }
 
-    public SimpleUserToken() {
+    public LocalUserToken() {
     }
 
     @Override
@@ -115,8 +115,8 @@ public class SimpleUserToken implements UserToken {
         this.type = type;
     }
 
-    public SimpleUserToken copy() {
-        SimpleUserToken userToken = new SimpleUserToken();
+    public LocalUserToken copy() {
+        LocalUserToken userToken = new LocalUserToken();
         userToken.firstRequestTime = firstRequestTime;
         userToken.lastRequestTime = lastRequestTime;
         userToken.requestTimesCounter = new AtomicLong(requestTimesCounter.get());

+ 0 - 92
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/RedisUserTokenManager.java

@@ -1,92 +0,0 @@
-package org.hswebframework.web.authorization.token;
-
-import org.springframework.data.redis.core.ReactiveHashOperations;
-import org.springframework.data.redis.core.ReactiveRedisOperations;
-import org.springframework.data.redis.core.ReactiveSetOperations;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-public class RedisUserTokenManager implements UserTokenManager {
-
-    ReactiveRedisOperations<Object, Object> operations;
-
-    ReactiveHashOperations<String, String, UserToken> userTokenStore;
-
-    ReactiveSetOperations<String, String> userTokenMapping;
-
-    Map<String,UserToken> localCache=new ConcurrentHashMap<>();
-
-    @Override
-    public Mono<UserToken> getByToken(String token) {
-
-        return null;
-    }
-
-    @Override
-    public Flux<UserToken> getByUserId(String userId) {
-        return null;
-    }
-
-    @Override
-    public Mono<Boolean> userIsLoggedIn(String userId) {
-        return null;
-    }
-
-    @Override
-    public Mono<Boolean> tokenIsLoggedIn(String token) {
-        return null;
-    }
-
-    @Override
-    public Mono<Integer> totalUser() {
-        return null;
-    }
-
-    @Override
-    public Mono<Integer> totalToken() {
-        return null;
-    }
-
-    @Override
-    public Flux<UserToken> allLoggedUser() {
-        return null;
-    }
-
-    @Override
-    public Mono<Void> signOutByUserId(String userId) {
-        return null;
-    }
-
-    @Override
-    public Mono<Void> signOutByToken(String token) {
-        return null;
-    }
-
-    @Override
-    public Mono<Void> changeUserState(String userId, TokenState state) {
-        return null;
-    }
-
-    @Override
-    public Mono<Void> changeTokenState(String token, TokenState state) {
-        return null;
-    }
-
-    @Override
-    public Mono<UserToken> signIn(String token, String type, String userId, long maxInactiveInterval) {
-        return null;
-    }
-
-    @Override
-    public Mono<Void> touch(String token) {
-        return null;
-    }
-
-    @Override
-    public Mono<Void> checkExpiredToken() {
-        return null;
-    }
-}

+ 233 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java

@@ -0,0 +1,233 @@
+package org.hswebframework.web.authorization.token.redis;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.collections.CollectionUtils;
+import org.hswebframework.web.authorization.exception.AccessDenyException;
+import org.hswebframework.web.authorization.token.AllopatricLoginMode;
+import org.hswebframework.web.authorization.token.TokenState;
+import org.hswebframework.web.authorization.token.UserToken;
+import org.hswebframework.web.authorization.token.UserTokenManager;
+import org.springframework.data.redis.core.ReactiveHashOperations;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+import org.springframework.data.redis.core.ReactiveSetOperations;
+import org.springframework.data.redis.core.ScanOptions;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class RedisUserTokenManager implements UserTokenManager {
+
+    private ReactiveRedisOperations<Object, Object> operations;
+
+    private ReactiveHashOperations<Object, String, Object> userTokenStore;
+
+    private ReactiveSetOperations<Object, Object> userTokenMapping;
+
+    public RedisUserTokenManager(ReactiveRedisOperations<Object, Object> operations) {
+        this.operations = operations;
+        this.userTokenStore = operations.opsForHash();
+        this.userTokenMapping = operations.opsForSet();
+    }
+
+    @Getter
+    @Setter
+    private Map<String, AllopatricLoginMode> allopatricLoginModes = new HashMap<>();
+
+    @Getter
+    @Setter
+    //异地登录模式,默认允许异地登录
+    private AllopatricLoginMode allopatricLoginMode = AllopatricLoginMode.allow;
+
+    private String getTokenRedisKey(String key) {
+        return "user-token:".concat(key);
+    }
+
+    private String getUserRedisKey(String key) {
+        return "user-token-user:".concat(key);
+    }
+
+    @Override
+    public Mono<UserToken> getByToken(String token) {
+        return userTokenStore
+                .entries(getTokenRedisKey(token))
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
+                .filter(map->!map.isEmpty())
+                .map(SimpleUserToken::of);
+    }
+
+    @Override
+    public Flux<UserToken> getByUserId(String userId) {
+        String redisKey = getUserRedisKey(userId);
+        return userTokenMapping
+                .members(redisKey)
+                .map(String::valueOf)
+                .flatMap(token -> getByToken(token)
+                        .switchIfEmpty(Mono.defer(() -> userTokenMapping
+                                .remove(redisKey, userId)
+                                .then(Mono.empty()))));
+    }
+
+    @Override
+    public Mono<Boolean> userIsLoggedIn(String userId) {
+        return getByUserId(userId)
+                .hasElements();
+    }
+
+    @Override
+    public Mono<Boolean> tokenIsLoggedIn(String token) {
+        return operations.hasKey(getTokenRedisKey(token));
+    }
+
+    @Override
+    public Mono<Integer> totalUser() {
+
+        return totalToken();
+    }
+
+    @Override
+    public Mono<Integer> totalToken() {
+        return operations.scan(ScanOptions
+                .scanOptions()
+                .match("user-token:*")
+                .build())
+                .count()
+                .map(Long::intValue);
+    }
+
+    @Override
+    public Flux<UserToken> allLoggedUser() {
+        return operations.scan(ScanOptions
+                .scanOptions()
+                .match("user-token:*")
+                .build())
+                .map(String::valueOf)
+                .flatMap(this::getByToken);
+    }
+
+    @Override
+    public Mono<Void> signOutByUserId(String userId) {
+        String key = getUserRedisKey(userId);
+        return getByUserId(key)
+                .map(UserToken::getToken)
+                .map(this::getTokenRedisKey)
+                .concatWithValues(key)
+                .as(operations::delete)
+                .then();
+    }
+
+    @Override
+    public Mono<Void> signOutByToken(String token) {
+        //delete token
+        // srem user token
+        return getByToken(token)
+                .flatMap(t -> operations.delete(getTokenRedisKey(t.getToken()))
+                        .then(userTokenMapping.remove(getUserRedisKey(t.getToken())))).then();
+    }
+
+    @Override
+    public Mono<Void> changeUserState(String userId, TokenState state) {
+
+        return getByUserId(userId)
+                .flatMap(token -> changeTokenState(token.getToken(), state))
+                .then();
+    }
+
+    @Override
+    public Mono<Void> changeTokenState(String token, TokenState state) {
+        return userTokenStore
+                .put(getTokenRedisKey(token), "state", state.getValue())
+                .then();
+    }
+
+    @Override
+    public Mono<UserToken> signIn(String token, String type, String userId, long maxInactiveInterval) {
+        return Mono.defer(() -> {
+            Mono<UserToken> doSign = Mono.defer(() -> {
+                Map<String, Object> map = new HashMap<>();
+                map.put("token", token);
+                map.put("type", type);
+                map.put("userId", userId);
+                map.put("maxInactiveInterval", maxInactiveInterval);
+                map.put("state", TokenState.normal.getValue());
+                map.put("signInTime", System.currentTimeMillis());
+                map.put("lastRequestTime", System.currentTimeMillis());
+
+                String key = getTokenRedisKey(token);
+                return userTokenStore
+                        .putAll(key, map)
+                        .then(Mono.defer(() -> {
+                            if (maxInactiveInterval > 0) {
+                                return operations.expire(key, Duration.ofMillis(maxInactiveInterval));
+                            }
+                            return Mono.empty();
+                        }))
+                        .then(userTokenMapping.add(getUserRedisKey(userId),token))
+                        .thenReturn(SimpleUserToken.of(map));
+            });
+
+            AllopatricLoginMode mode = allopatricLoginModes.getOrDefault(type, allopatricLoginMode);
+            if (mode == AllopatricLoginMode.deny) {
+                return userIsLoggedIn(userId)
+                        .flatMap(r -> {
+                            if (r) {
+                                return Mono.error(new AccessDenyException("已在其他地方登录", TokenState.deny.getValue(), null));
+                            }
+                            return doSign;
+                        });
+
+            } else if (mode == AllopatricLoginMode.offlineOther) {
+                return getByUserId(userId)
+                        .flatMap(userToken -> {
+                            if (type.equals(userToken.getType())) {
+                                return this.changeTokenState(userToken.getToken(), TokenState.offline);
+                            }
+                            return Mono.empty();
+                        })
+                        .then(doSign);
+            }
+
+            return doSign;
+        });
+    }
+
+
+    @Override
+    public Mono<Void> touch(String token) {
+        return getByToken(token)
+                .flatMap(userToken -> {
+                    if (userToken.getMaxInactiveInterval() > 0) {
+                        return userTokenStore
+                                .increment(getTokenRedisKey(token), token, 1L)
+                                .then(operations
+                                        .expire(getTokenRedisKey(token), Duration.ofMillis(userToken.getMaxInactiveInterval()))
+                                        .then());
+                    }
+                    return Mono.empty();
+                });
+    }
+
+    @Override
+    public Mono<Void> checkExpiredToken() {
+
+        return operations.scan(ScanOptions
+                .scanOptions()
+                .match("user-token-user:*").build())
+                .map(String::valueOf)
+                .flatMap(key -> userTokenMapping.members(key)
+                        .map(String::valueOf)
+                        .flatMap(token -> operations.hasKey(getTokenRedisKey(token))
+                                .flatMap(exists -> {
+                                    if (!exists) {
+                                        return userTokenMapping.remove(key, token);
+                                    }
+                                    return Mono.empty();
+                                })))
+                .then();
+    }
+}

+ 37 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/SimpleUserToken.java

@@ -0,0 +1,37 @@
+package org.hswebframework.web.authorization.token.redis;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.hswebframework.web.authorization.token.TokenState;
+import org.hswebframework.web.authorization.token.UserToken;
+import org.hswebframework.web.bean.FastBeanCopier;
+
+import java.util.Map;
+
+@Getter
+@Setter
+@ToString(exclude = "token")
+public class SimpleUserToken implements UserToken {
+
+    private String userId;
+
+    private String token;
+
+    private long requestTimes;
+
+    private long lastRequestTime;
+
+    private long signInTime;
+
+    private TokenState state;
+
+    private String type;
+
+    private long maxInactiveInterval;
+
+    public static SimpleUserToken of(Map<String, Object> map) {
+
+        return FastBeanCopier.copy(map, new SimpleUserToken());
+    }
+}

+ 139 - 0
hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManagerTest.java

@@ -0,0 +1,139 @@
+package org.hswebframework.web.authorization.token.redis;
+
+import lombok.SneakyThrows;
+import org.hswebframework.web.authorization.exception.AccessDenyException;
+import org.hswebframework.web.authorization.exception.UnAuthorizedException;
+import org.hswebframework.web.authorization.token.AllopatricLoginMode;
+import org.hswebframework.web.authorization.token.TokenState;
+import org.hswebframework.web.authorization.token.UserToken;
+import org.hswebframework.web.authorization.token.UserTokenManager;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.ReactiveRedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.util.HashMap;
+
+import static org.junit.Assert.*;
+
+public class RedisUserTokenManagerTest {
+
+    UserTokenManager tokenManager;
+
+    @Before
+    public void init() {
+        LettuceConnectionFactory factory = new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1"));
+
+        ReactiveRedisTemplate<Object, Object> template = new ReactiveRedisTemplate<>(
+                factory,
+                RedisSerializationContext.java()
+        );
+        factory.afterPropertiesSet();
+
+        RedisUserTokenManager tokenManager = new RedisUserTokenManager(template);
+        this.tokenManager = tokenManager;
+        tokenManager.setAllopatricLoginModes(new HashMap<String, AllopatricLoginMode>() {
+            {
+                put("offline", AllopatricLoginMode.offlineOther);
+                put("deny", AllopatricLoginMode.deny);
+            }
+        });
+    }
+
+    @Test
+    public void testSign() {
+
+        tokenManager.signIn("test-token", "test", "test", 10000)
+                .map(UserToken::getToken)
+                .as(StepVerifier::create)
+                .expectNext("test-token")
+                .verifyComplete();
+
+        tokenManager.userIsLoggedIn("test")
+                .as(StepVerifier::create)
+                .expectNext(true)
+                .verifyComplete();
+
+        tokenManager.tokenIsLoggedIn("test-token")
+                .as(StepVerifier::create)
+                .expectNext(true)
+                .verifyComplete();
+
+        tokenManager.getByToken("test-token")
+                .map(UserToken::getState)
+                .as(StepVerifier::create)
+                .expectNext(TokenState.normal)
+                .verifyComplete();
+    }
+
+
+    @Test
+    @SneakyThrows
+    public void testOfflineOther() {
+        tokenManager.signIn("test-token_offline1", "offline", "user1", 1000)
+                .map(UserToken::getToken)
+                .as(StepVerifier::create)
+                .expectNext("test-token_offline1")
+                .verifyComplete();
+
+        tokenManager.signIn("test-token_offline2", "offline", "user1", 1000)
+                .map(UserToken::getToken)
+                .as(StepVerifier::create)
+                .expectNext("test-token_offline2")
+                .verifyComplete();
+
+        tokenManager.getByToken("test-token_offline1")
+                .map(UserToken::getState)
+                .as(StepVerifier::create)
+                .expectNext(TokenState.offline)
+                .verifyComplete();
+    }
+
+    @Test
+    @SneakyThrows
+    public void testDeny() {
+        tokenManager.signIn("test-token_offline3", "deny", "user2", 1000)
+                .map(UserToken::getToken)
+                .as(StepVerifier::create)
+                .expectNext("test-token_offline3")
+                .verifyComplete();
+
+        tokenManager.signIn("test-token_offline4", "deny", "user2", 1000)
+                .map(UserToken::getToken)
+                .as(StepVerifier::create)
+                .expectError(AccessDenyException.class)
+                .verify();
+    }
+
+    @Test
+    @SneakyThrows
+    public void testSignTimeout() {
+        tokenManager.signIn("test-token_2", "test", "test2", 1000)
+                .map(UserToken::getToken)
+                .as(StepVerifier::create)
+                .expectNext("test-token_2")
+                .verifyComplete();
+
+        tokenManager.touch("test-token_2")
+                .as(StepVerifier::create)
+                .expectFusion()
+                .verifyComplete();
+
+        Thread.sleep(2000);
+        tokenManager.getByToken("test-token_2")
+                .switchIfEmpty(Mono.error(new UnAuthorizedException()))
+                .as(StepVerifier::create)
+                .expectError(UnAuthorizedException.class)
+                .verify();
+
+        tokenManager.getByUserId("test2")
+                .count()
+                .as(StepVerifier::create)
+                .expectNext(0L)
+                .verifyComplete();
+    }
+}