diff --git a/hsweb-authorization/hsweb-authorization-api/pom.xml b/hsweb-authorization/hsweb-authorization-api/pom.xml index 10c12a3ec..4e6f2c446 100644 --- a/hsweb-authorization/hsweb-authorization-api/pom.xml +++ b/hsweb-authorization/hsweb-authorization-api/pom.xml @@ -5,7 +5,7 @@ hsweb-authorization org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 @@ -43,7 +43,7 @@ - io.swagger + io.swagger.core.v3 swagger-annotations diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java index ebb5b5b3d..3719c58a1 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java @@ -22,6 +22,8 @@ import reactor.core.publisher.Mono; import java.io.Serializable; import java.util.*; +import java.util.function.BiPredicate; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -207,4 +209,13 @@ public interface Authentication extends Serializable { */ Authentication merge(Authentication source); + /** + * copy为新的权限信息 + * + * @param permissionFilter 权限过滤 + * @param dimension 维度过滤 + * @return 新的权限信息 + */ + Authentication copy(BiPredicate permissionFilter, + Predicate dimension); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java index 803f38e3c..d8d9fb076 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Permission.java @@ -153,7 +153,7 @@ public interface Permission extends Serializable { * @see FieldFilterDataAccessConfig#getFields() */ default Optional findFieldFilter(String action) { - return findDataAccess(conf -> FieldFilterDataAccessConfig.class.isInstance(conf) && conf.getAction().equals(action)); + return findDataAccess(conf -> conf instanceof FieldFilterDataAccessConfig && conf.getAction().equals(action)); } /** @@ -164,7 +164,7 @@ public interface Permission extends Serializable { */ default Set findDenyFields(String action) { return findFieldFilter(action) - .filter(conf -> DENY_FIELDS.equals(conf.getType())) + .filter(conf -> DENY_FIELDS.equals(conf.getType().getId())) .map(FieldFilterDataAccessConfig::getFields) .orElseGet(Collections::emptySet); } @@ -210,6 +210,8 @@ public interface Permission extends Serializable { Permission copy(); + Permission copy(Predicate actionFilter,Predicate dataAccessFilter); + /** * 数据权限查找判断逻辑接口 * diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java index befc744d9..229ef8c50 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java @@ -6,10 +6,7 @@ import org.hswebframework.web.authorization.builder.DataAccessConfigBuilderFacto import org.hswebframework.web.authorization.simple.builder.DataAccessConfigConverter; import org.hswebframework.web.authorization.simple.builder.SimpleAuthenticationBuilderFactory; import org.hswebframework.web.authorization.simple.builder.SimpleDataAccessConfigBuilderFactory; -import org.hswebframework.web.authorization.token.DefaultUserTokenManager; -import org.hswebframework.web.authorization.token.UserTokenAuthenticationSupplier; -import org.hswebframework.web.authorization.token.UserTokenReactiveAuthenticationSupplier; -import org.hswebframework.web.authorization.token.UserTokenManager; +import org.hswebframework.web.authorization.token.*; import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; import org.hswebframework.web.authorization.twofactor.defaults.DefaultTwoFactorValidatorManager; import org.hswebframework.web.convert.CustomMessageConverter; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java index c9d0318a7..356ec9d4e 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimpleAuthentication.java @@ -23,7 +23,9 @@ import org.hswebframework.web.authorization.*; import java.io.Serializable; import java.util.*; +import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; @Getter @@ -40,9 +42,10 @@ public class SimpleAuthentication implements Authentication { private Map attributes = new HashMap<>(); - public static Authentication of(){ + public static Authentication of() { return new SimpleAuthentication(); } + @Override @SuppressWarnings("unchecked") public Optional getAttribute(String name) { @@ -77,4 +80,19 @@ public class SimpleAuthentication implements Authentication { } return this; } + + @Override + public Authentication copy(BiPredicate permissionFilter, + Predicate dimension) { + SimpleAuthentication authentication = new SimpleAuthentication(); + authentication.setUser(user); + authentication.setDimensions(dimensions.stream().filter(dimension).collect(Collectors.toList())); + authentication.setPermissions(permissions + .stream() + .map(permission -> permission.copy(action -> permissionFilter.test(permission, action), conf -> true)) + .filter(per -> !per.getActions().isEmpty()) + .collect(Collectors.toList()) + ); + return authentication; + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java index 098efb7d5..c2d46908c 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/SimplePermission.java @@ -5,6 +5,8 @@ import org.hswebframework.web.authorization.Permission; import org.hswebframework.web.authorization.access.DataAccessConfig; import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * @author zhouhao @@ -42,16 +44,22 @@ public class SimplePermission implements Permission { return dataAccesses; } - public Permission copy() { + @Override + public Permission copy(Predicate actionFilter, + Predicate dataAccessFilter) { SimplePermission permission = new SimplePermission(); permission.setId(id); permission.setName(name); - permission.setActions(new HashSet<>(getActions())); - permission.setDataAccesses(new HashSet<>(getDataAccesses())); + permission.setActions(getActions().stream().filter(actionFilter).collect(Collectors.toSet())); + permission.setDataAccesses(getDataAccesses().stream().filter(dataAccessFilter).collect(Collectors.toSet())); if (options != null) { permission.setOptions(new HashMap<>(options)); } return permission; } + + public Permission copy() { + return copy(action -> true, conf -> true); + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java index 8bb30a009..e058775da 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ParsedToken.java @@ -15,4 +15,8 @@ public interface ParsedToken { * @return 令牌类型 */ String getType(); + + static ParsedToken of(String type, String token) { + return SimpleParsedToken.of(type, token); + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ReactiveTokenAuthenticationSupplier.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ReactiveTokenAuthenticationSupplier.java new file mode 100644 index 000000000..bd7b59516 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/ReactiveTokenAuthenticationSupplier.java @@ -0,0 +1,32 @@ +package org.hswebframework.web.authorization.token; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.ReactiveAuthenticationSupplier; +import org.hswebframework.web.context.ContextKey; +import org.hswebframework.web.context.ContextUtils; +import org.hswebframework.web.logger.ReactiveLogger; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class ReactiveTokenAuthenticationSupplier implements ReactiveAuthenticationSupplier { + + private final TokenAuthenticationManager tokenManager; + + @Override + public Mono get(String userId) { + return Mono.empty(); + } + + @Override + public Mono get() { + return ContextUtils.reactiveContext() + .flatMap(context -> + context.get(ContextKey.of(ParsedToken.class)) + .map(t -> tokenManager.getByToken(t.getToken())) + .orElseGet(Mono::empty)) + .flatMap(auth -> ReactiveLogger.mdc("userId", auth.getUser().getId()) + .then(ReactiveLogger.mdc("username", auth.getUser().getName())) + .thenReturn(auth)); + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java new file mode 100644 index 000000000..cedcac0cd --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/SimpleParsedToken.java @@ -0,0 +1,17 @@ +package org.hswebframework.web.authorization.token; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor(staticName = "of") +public class SimpleParsedToken implements ParsedToken{ + + private String type; + + private String token; + + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenAuthenticationManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenAuthenticationManager.java new file mode 100644 index 000000000..c806a9353 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenAuthenticationManager.java @@ -0,0 +1,40 @@ +package org.hswebframework.web.authorization.token; + +import org.hswebframework.web.authorization.Authentication; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * token 权限管理器,根据token来进行权限关联. + * + * @author zhouhao + * @since 4.0.7 + */ +public interface TokenAuthenticationManager { + + /** + * 根据token获取认证信息 + * + * @param token token + * @return 认证信息 + */ + Mono getByToken(String token); + + /** + * 设置token认证信息 + * + * @param token token + * @param auth 认证信息 + * @param ttl 有效期 + * @return void + */ + Mono putAuthentication(String token, Authentication auth, Duration ttl); + + /** + * 删除token + * @param token token + * @return void + */ + Mono removeToken(String token); +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java index 2b400cf46..f43423ad1 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/UserTokenReactiveAuthenticationSupplier.java @@ -19,13 +19,14 @@ import java.util.Map; */ public class UserTokenReactiveAuthenticationSupplier implements ReactiveAuthenticationSupplier { - private ReactiveAuthenticationManager defaultAuthenticationManager; + private final ReactiveAuthenticationManager defaultAuthenticationManager; - private UserTokenManager userTokenManager; + private final UserTokenManager userTokenManager; - private Map thirdPartAuthenticationManager = new HashMap<>(); + private final Map thirdPartAuthenticationManager = new HashMap<>(); - public UserTokenReactiveAuthenticationSupplier(UserTokenManager userTokenManager, ReactiveAuthenticationManager defaultAuthenticationManager) { + public UserTokenReactiveAuthenticationSupplier(UserTokenManager userTokenManager, + ReactiveAuthenticationManager defaultAuthenticationManager) { this.defaultAuthenticationManager = defaultAuthenticationManager; this.userTokenManager = userTokenManager; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java index 2606c4b9b..b78184dff 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/event/UserTokenChangedEvent.java @@ -5,7 +5,7 @@ import org.hswebframework.web.authorization.token.UserToken; import org.springframework.context.ApplicationEvent; public class UserTokenChangedEvent extends ApplicationEvent implements AuthorizationEvent { - private UserToken before, after; + private final UserToken before, after; public UserTokenChangedEvent(UserToken before, UserToken after) { super(after); diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisTokenAuthenticationManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisTokenAuthenticationManager.java new file mode 100644 index 000000000..31874ea98 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisTokenAuthenticationManager.java @@ -0,0 +1,61 @@ +package org.hswebframework.web.authorization.token.redis; + +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.token.TokenAuthenticationManager; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisOperations; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +public class RedisTokenAuthenticationManager implements TokenAuthenticationManager { + + private final ReactiveRedisOperations operations; + + @SuppressWarnings("all") + public RedisTokenAuthenticationManager(ReactiveRedisConnectionFactory connectionFactory) { + this(new ReactiveRedisTemplate<>( + connectionFactory, RedisSerializationContext.newSerializationContext() + .key(RedisSerializer.string()) + .value((RedisSerializer) RedisSerializer.java()) + .hashKey(RedisSerializer.string()) + .hashValue(RedisSerializer.java()) + .build() + )); + } + + public RedisTokenAuthenticationManager(ReactiveRedisOperations operations) { + this.operations = operations; + } + + @Override + public Mono getByToken(String token) { + return operations + .opsForValue() + .get("token-auth:" + token); + } + + @Override + public Mono removeToken(String token) { + return operations + .delete(token) + .then(); + } + + @Override + public Mono putAuthentication(String token, Authentication auth, Duration ttl) { + return ttl.isNegative() + ? operations + .opsForValue() + .set("token-auth:" + token, auth) + .then() + : operations + .opsForValue() + .set("token-auth:" + token, auth, ttl) + .then() + ; + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java index ab7327114..06e2916e2 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java @@ -2,21 +2,24 @@ 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 org.hswebframework.web.authorization.token.event.UserTokenChangedEvent; +import org.hswebframework.web.authorization.token.event.UserTokenCreatedEvent; +import org.hswebframework.web.authorization.token.event.UserTokenRemovedEvent; +import org.hswebframework.web.bean.FastBeanCopier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; 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; @@ -35,6 +38,18 @@ public class RedisUserTokenManager implements UserTokenManager { this.userTokenMapping = operations.opsForSet(); } + @SuppressWarnings("all") + public RedisUserTokenManager(ReactiveRedisConnectionFactory connectionFactory) { + this(new ReactiveRedisTemplate<>(connectionFactory, + RedisSerializationContext.newSerializationContext() + .key((RedisSerializer) RedisSerializer.string()) + .value(RedisSerializer.java()) + .hashKey(RedisSerializer.string()) + .hashValue(RedisSerializer.java()) + .build() + )); + } + @Getter @Setter private Map allopatricLoginModes = new HashMap<>(); @@ -44,6 +59,9 @@ public class RedisUserTokenManager implements UserTokenManager { //异地登录模式,默认允许异地登录 private AllopatricLoginMode allopatricLoginMode = AllopatricLoginMode.allow; + @Setter + private ApplicationEventPublisher eventPublisher; + private String getTokenRedisKey(String key) { return "user-token:".concat(key); } @@ -114,20 +132,24 @@ public class RedisUserTokenManager implements UserTokenManager { public Mono signOutByUserId(String userId) { String key = getUserRedisKey(userId); return getByUserId(key) - .map(UserToken::getToken) - .map(this::getTokenRedisKey) - .concatWithValues(key) - .as(operations::delete) + .flatMap(userToken -> operations + .delete(getTokenRedisKey(userToken.getToken())) + .then(onTokenRemoved(userToken))) + .then(operations.delete(key)) .then(); } @Override public Mono signOutByToken(String token) { //delete token - // srem user token + //srem user token return getByToken(token) - .flatMap(t -> operations.delete(getTokenRedisKey(t.getToken())) - .then(userTokenMapping.remove(getUserRedisKey(t.getToken()),token))).then(); + .flatMap(t -> operations + .delete(getTokenRedisKey(t.getToken())) + .then(userTokenMapping.remove(getUserRedisKey(t.getToken()), token)) + .then(onTokenRemoved(t)) + ) + .then(); } @Override @@ -140,60 +162,68 @@ public class RedisUserTokenManager implements UserTokenManager { @Override public Mono changeTokenState(String token, TokenState state) { - return userTokenStore - .put(getTokenRedisKey(token), "state", state.getValue()) - .then(); + + return getByToken(token) + .flatMap(old -> { + SimpleUserToken newToken = FastBeanCopier.copy(old, new SimpleUserToken()); + newToken.setState(state); + return userTokenStore + .put(getTokenRedisKey(token), "state", state.getValue()) + .then(onTokenChanged(old, newToken)); + }); } @Override public Mono signIn(String token, String type, String userId, long maxInactiveInterval) { - return Mono.defer(() -> { - Mono doSign = Mono.defer(() -> { - Map 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()); + return Mono + .defer(() -> { + Mono doSign = Mono.defer(() -> { + Map 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)); - }); + 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; - }); + 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); - } + } 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; - }); + return doSign; + }) + .flatMap(this::onUserTokenCreated); } @@ -213,9 +243,8 @@ public class RedisUserTokenManager implements UserTokenManager { @Override public Mono checkExpiredToken() { - return operations.scan(ScanOptions - .scanOptions() - .match("user-token-user:*").build()) + return operations + .scan(ScanOptions.scanOptions().match("user-token-user:*").build()) .map(String::valueOf) .flatMap(key -> userTokenMapping.members(key) .map(String::valueOf) @@ -228,4 +257,28 @@ public class RedisUserTokenManager implements UserTokenManager { }))) .then(); } + + private Mono onTokenRemoved(UserToken token) { + if (eventPublisher == null) { + return Mono.empty(); + } + return Mono.fromRunnable(() -> eventPublisher.publishEvent(new UserTokenRemovedEvent(token))); + } + + private Mono onTokenChanged(UserToken old, UserToken newToken) { + if (eventPublisher == null) { + return Mono.empty(); + } + return Mono.fromRunnable(() -> eventPublisher.publishEvent(new UserTokenChangedEvent(old, newToken))); + } + + private Mono onUserTokenCreated(UserToken token) { + if (eventPublisher == null) { + return Mono.just(token); + } + return Mono + .fromRunnable(() -> eventPublisher.publishEvent(new UserTokenCreatedEvent(token))) + .thenReturn(token); + } + } diff --git a/hsweb-authorization/hsweb-authorization-basic/pom.xml b/hsweb-authorization/hsweb-authorization-basic/pom.xml index 169cd742b..59a230c6b 100644 --- a/hsweb-authorization/hsweb-authorization-basic/pom.xml +++ b/hsweb-authorization/hsweb-authorization-basic/pom.xml @@ -5,7 +5,7 @@ hsweb-authorization org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java index 23a3bd2d4..472b074be 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java @@ -17,11 +17,8 @@ package org.hswebframework.web.authorization.basic.web; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.SneakyThrows; import org.hswebframework.web.authorization.Authentication; @@ -33,12 +30,10 @@ import org.hswebframework.web.authorization.events.AuthorizationFailedEvent; import org.hswebframework.web.authorization.events.AuthorizationSuccessEvent; import org.hswebframework.web.authorization.exception.AuthenticationException; import org.hswebframework.web.authorization.exception.UnAuthorizedException; -import org.hswebframework.web.authorization.simple.CompositeReactiveAuthenticationManager; import org.hswebframework.web.authorization.simple.PlainTextUsernamePasswordAuthenticationRequest; import org.hswebframework.web.logging.AccessLogger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.repository.query.Param; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; @@ -71,7 +66,6 @@ public class AuthorizationController { } @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) - @ApiOperation("用户名密码登录,json方式") @Authorize(ignore = true) @AccessLogger(ignore = true) @Operation(summary = "登录",description = "必要参数:username,password.根据配置不同,其他参数也不同,如:验证码等.") diff --git a/hsweb-authorization/hsweb-authorization-oauth2/pom.xml b/hsweb-authorization/hsweb-authorization-oauth2/pom.xml new file mode 100644 index 000000000..657a94897 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/pom.xml @@ -0,0 +1,52 @@ + + + + hsweb-authorization + org.hswebframework.web + 4.0.8-SNAPSHOT + + 4.0.0 + + hsweb-authorization-oauth2 + + + + org.hswebframework.web + hsweb-authorization-api + ${project.version} + + + + io.projectreactor + reactor-core + + + + org.springframework + spring-webflux + true + + + + org.springframework.data + spring-data-redis + true + + + + io.lettuce + lettuce-core + test + + + + org.hswebframework.web + hsweb-authorization-basic + ${project.version} + true + + + + \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/ErrorType.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/ErrorType.java new file mode 100644 index 000000000..92bba479d --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/ErrorType.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020 http://www.hswebframework.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package org.hswebframework.web.oauth2; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum ErrorType { + ILLEGAL_CODE(1001), //错误的授权码 + ILLEGAL_ACCESS_TOKEN(1002), //错误的access_token + ILLEGAL_CLIENT_ID(1003),//客户端信息错误 + ILLEGAL_CLIENT_SECRET(1004),//客户端密钥错误 + ILLEGAL_GRANT_TYPE(1005), //错误的授权方式 + ILLEGAL_RESPONSE_TYPE(1006),//response_type 错误 + ILLEGAL_AUTHORIZATION(1007),//Authorization 错误 + ILLEGAL_REFRESH_TOKEN(1008),//refresh_token 错误 + ILLEGAL_REDIRECT_URI(1009), //redirect_url 错误 + ILLEGAL_SCOPE(1010), //scope 错误 + ILLEGAL_USERNAME(1011), //username 错误 + ILLEGAL_PASSWORD(1012), //password 错误 + + SCOPE_OUT_OF_RANGE(2010), //scope超出范围 + + UNAUTHORIZED_CLIENT(4010), //无权限 + EXPIRED_TOKEN(4011), //TOKEN过期 + INVALID_TOKEN(4012), //TOKEN已失效 + UNSUPPORTED_GRANT_TYPE(4013), //不支持的认证类型 + UNSUPPORTED_RESPONSE_TYPE(4014), //不支持的响应类型 + + EXPIRED_CODE(4015), //AUTHORIZATION_CODE过期 + EXPIRED_REFRESH_TOKEN(4020), //REFRESH_TOKEN过期 + + CLIENT_DISABLED(4016),//客户端已被禁用 + + CLIENT_NOT_EXIST(4040),//客户端不存在 + + USER_NOT_EXIST(4041),//客户端不存在 + + STATE_ERROR(4042), //stat错误 + + ACCESS_DENIED(503), //访问被拒绝 + + OTHER(5001), //其他错误 ; + + PARSE_RESPONSE_ERROR(5002),//解析返回结果错误 + + SERVICE_ERROR(5003); //服务器返回错误信息 + + + private final String message; + private final int code; + static final Map codeMapping = Arrays.stream(ErrorType.values()) + .collect(Collectors.toMap(ErrorType::code, type -> type)); + + ErrorType(int code) { + this.code = code; + message = this.name().toLowerCase(); + } + + ErrorType(int code, String message) { + this.message = message; + this.code = code; + } + + public String message() { + if (message == null) { + return this.name(); + } + return message; + } + + public int code() { + return code; + } + + public T throwThis(Function errorTypeFunction) { + throw errorTypeFunction.apply(this); + } + + public T throwThis(BiFunction errorTypeFunction, String message) { + throw errorTypeFunction.apply(this, message); + } + + public static Optional fromCode(int code) { + return Optional.ofNullable(codeMapping.get(code)); + } + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/GrantType.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/GrantType.java new file mode 100644 index 000000000..d8d25ac1b --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/GrantType.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 http://www.hswebframework.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package org.hswebframework.web.oauth2; + +/** + * + * @author zhouhao + */ +public interface GrantType { + String authorization_code = "authorization_code"; + String implicit = "implicit"; + @SuppressWarnings("all") + String password = "password"; + String client_credentials = "client_credentials"; + String refresh_token = "refresh_token"; +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Constants.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Constants.java new file mode 100644 index 000000000..86c986de5 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Constants.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 http://www.hswebframework.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package org.hswebframework.web.oauth2; + +/** + * @author zhouhao + */ +public interface OAuth2Constants { + String access_token = "access_token"; + String refresh_token = "refresh_token"; + String grant_type = "grant_type"; + String scope = "scope"; + String client_id = "client_id"; + String client_secret = "client_secret"; + String authorization = "Authorization"; + String redirect_uri = "redirect_uri"; + String response_type = "response_type"; + String state = "state"; + String code = "code"; + String username = "username"; + + @SuppressWarnings("all") + String password = "password"; + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java new file mode 100644 index 000000000..fc48ad3e1 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java @@ -0,0 +1,19 @@ +package org.hswebframework.web.oauth2; + +import lombok.Getter; +import org.hswebframework.web.exception.BusinessException; + +@Getter +public class OAuth2Exception extends BusinessException { + private final ErrorType type; + + public OAuth2Exception(ErrorType type) { + super(type.message(), type.name(), type.code()); + this.type = type; + } + + public OAuth2Exception(String message, Throwable cause, ErrorType type) { + super(message, cause); + this.type = type; + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/ResponseType.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/ResponseType.java new file mode 100644 index 000000000..72fb5918c --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/ResponseType.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 http://www.hswebframework.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package org.hswebframework.web.oauth2; + +/** + * TODO 完成注释 + * + * @author zhouhao + */ +public interface ResponseType { + String code = "code"; + String token = "token"; +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessToken.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessToken.java new file mode 100644 index 000000000..a4c1956c8 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessToken.java @@ -0,0 +1,28 @@ +package org.hswebframework.web.oauth2.server; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class AccessToken extends OAuth2Response { + + private static final long serialVersionUID = -6849794470754667710L; + + @Schema(name="access_token") + @JsonProperty("access_token") + private String accessToken; + + @Schema(name="refresh_token") + @JsonProperty("refresh_token") + private String refreshToken; + + @Schema(name="expires_in") + @JsonProperty("expires_in") + private int expiresIn; + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java new file mode 100644 index 000000000..1645357d3 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/AccessTokenManager.java @@ -0,0 +1,16 @@ +package org.hswebframework.web.oauth2.server; + +import org.hswebframework.web.authorization.Authentication; +import reactor.core.publisher.Mono; + +public interface AccessTokenManager { + + Mono getAuthenticationByToken(String accessToken); + + Mono createAccessToken(String clientId, + Authentication authentication, + boolean singleton); + + Mono refreshAccessToken(String clientId, String refreshToken); + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/ClientCredentialGranter.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/ClientCredentialGranter.java new file mode 100644 index 000000000..cee9c7ca6 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/ClientCredentialGranter.java @@ -0,0 +1,7 @@ +package org.hswebframework.web.oauth2.server; + +public interface ClientCredentialGranter extends OAuth2Granter { + + + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java new file mode 100644 index 000000000..6421546de --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Client.java @@ -0,0 +1,38 @@ +package org.hswebframework.web.oauth2.server; + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.oauth2.ErrorType; +import org.hswebframework.web.oauth2.OAuth2Exception; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.NotBlank; + +@Getter +@Setter +public class OAuth2Client { + + @NotBlank + private String clientId; + + @NotBlank + private String clientSecret; + + @NotBlank + private String name; + + private String description; + + @NotBlank + private String redirectUrl; + + //client 所属用户 + private String userId; + + public void validateRedirectUri(String redirectUri) { + if (StringUtils.isEmpty(redirectUri) || (!redirectUri.startsWith(this.redirectUrl))) { + throw new OAuth2Exception(ErrorType.ILLEGAL_REDIRECT_URI); + } + } + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ClientManager.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ClientManager.java new file mode 100644 index 000000000..eb6d5b9df --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ClientManager.java @@ -0,0 +1,9 @@ +package org.hswebframework.web.oauth2.server; + +import reactor.core.publisher.Mono; + +public interface OAuth2ClientManager { + + Mono getClient(String clientId); + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2GrantService.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2GrantService.java new file mode 100644 index 000000000..0b1feebf1 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2GrantService.java @@ -0,0 +1,12 @@ +package org.hswebframework.web.oauth2.server; + + +import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter; + +public interface OAuth2GrantService { + + AuthorizationCodeGranter authorizationCode(); + + ClientCredentialGranter clientCredential(); + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Granter.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Granter.java new file mode 100644 index 000000000..e9ee66b1b --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Granter.java @@ -0,0 +1,7 @@ +package org.hswebframework.web.oauth2.server; + +public interface OAuth2Granter { + + + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Request.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Request.java new file mode 100644 index 000000000..ab21e5753 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Request.java @@ -0,0 +1,31 @@ +package org.hswebframework.web.oauth2.server; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Getter +@Setter +@AllArgsConstructor +public class OAuth2Request { + + private Map parameters; + + + public Optional getParameter(String key) { + return Optional.ofNullable(parameters) + .map(params -> params.get(key)); + } + + public OAuth2Request with(String parameter, Object key) { + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.put(parameter, key); + return this; + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Response.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Response.java new file mode 100644 index 000000000..a6b80098c --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2Response.java @@ -0,0 +1,25 @@ +package org.hswebframework.web.oauth2.server; + +import io.swagger.v3.oas.annotations.Hidden; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +public class OAuth2Response implements Serializable { + @Hidden + private Map parameters; + + public OAuth2Response with(String parameter, Object key) { + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.put(parameter, key); + return this; + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java new file mode 100644 index 000000000..51c979d50 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/OAuth2ServerAutoConfiguration.java @@ -0,0 +1,77 @@ +package org.hswebframework.web.oauth2.server; + +import org.hswebframework.web.authorization.ReactiveAuthenticationHolder; +import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser; +import org.hswebframework.web.oauth2.server.auth.ReactiveOAuth2AccessTokenParser; +import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter; +import org.hswebframework.web.oauth2.server.code.DefaultAuthorizationCodeGranter; +import org.hswebframework.web.oauth2.server.impl.CompositeOAuth2GrantService; +import org.hswebframework.web.oauth2.server.impl.RedisAccessTokenManager; +import org.hswebframework.web.oauth2.server.web.OAuth2AuthorizeController; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; + +@Configuration(proxyBeanMethods = false) +public class OAuth2ServerAutoConfiguration { + + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ReactiveUserTokenParser.class) + static class ReactiveOAuth2AccessTokenParserConfiguration { + + @Bean + @ConditionalOnBean(AccessTokenManager.class) + public ReactiveOAuth2AccessTokenParser reactiveOAuth2AccessTokenParser(AccessTokenManager accessTokenManager) { + ReactiveOAuth2AccessTokenParser parser = new ReactiveOAuth2AccessTokenParser(accessTokenManager); + ReactiveAuthenticationHolder.addSupplier(parser); + return parser; + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + static class ReactiveOAuth2ServerAutoConfiguration { + + + @Bean + @ConditionalOnMissingBean + public AccessTokenManager accessTokenManager(ReactiveRedisConnectionFactory redisConnectionFactory) { + return new RedisAccessTokenManager(redisConnectionFactory); + } + + + @Bean + @ConditionalOnMissingBean + public AuthorizationCodeGranter authorizationCodeGranter(AccessTokenManager tokenManager, + ReactiveRedisConnectionFactory redisConnectionFactory) { + return new DefaultAuthorizationCodeGranter(tokenManager, redisConnectionFactory); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2GrantService oAuth2GrantService(ObjectProvider codeProvider, + ObjectProvider credentialProvider) { + CompositeOAuth2GrantService grantService = new CompositeOAuth2GrantService(); + grantService.setAuthorizationCodeGranter(codeProvider.getIfAvailable()); + grantService.setClientCredentialGranter(credentialProvider.getIfAvailable()); + + return grantService; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(OAuth2ClientManager.class) + public OAuth2AuthorizeController oAuth2AuthorizeController(OAuth2GrantService grantService, + OAuth2ClientManager clientManager) { + return new OAuth2AuthorizeController(grantService, clientManager); + } + + } + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/ScopePredicate.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/ScopePredicate.java new file mode 100644 index 000000000..ca55e43da --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/ScopePredicate.java @@ -0,0 +1,10 @@ +package org.hswebframework.web.oauth2.server; + +import java.util.function.BiPredicate; + +@FunctionalInterface +public interface ScopePredicate extends BiPredicate { + + boolean test(String permission, String... actions); + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java new file mode 100644 index 000000000..7f1ab25a8 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/auth/ReactiveOAuth2AccessTokenParser.java @@ -0,0 +1,61 @@ +package org.hswebframework.web.oauth2.server.auth; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.ReactiveAuthenticationSupplier; +import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser; +import org.hswebframework.web.authorization.token.ParsedToken; +import org.hswebframework.web.context.ContextKey; +import org.hswebframework.web.context.ContextUtils; +import org.hswebframework.web.logger.ReactiveLogger; +import org.hswebframework.web.oauth2.server.AccessTokenManager; +import org.springframework.http.HttpHeaders; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class ReactiveOAuth2AccessTokenParser implements ReactiveUserTokenParser, ReactiveAuthenticationSupplier { + + private final AccessTokenManager accessTokenManager; + + @Override + public Mono parseToken(ServerWebExchange exchange) { + + String token = exchange.getRequest().getQueryParams().getFirst("access_token"); + if (StringUtils.isEmpty(token)) { + token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(token)) { + String[] typeAndToken = token.split("[ ]"); + if (typeAndToken.length == 2 && typeAndToken[0].equalsIgnoreCase("bearer")) { + token = typeAndToken[1]; + } + } + } + + if (StringUtils.hasText(token)) { + return Mono.just(ParsedToken.of("oauth2", token)); + } + + return Mono.empty(); + } + + @Override + public Mono get(String userId) { + return Mono.empty(); + } + + @Override + public Mono get() { + return ContextUtils.reactiveContext() + .flatMap(context -> + context.get(ContextKey.of(ParsedToken.class)) + .filter(token -> "oauth2".equals(token.getType())) + .map(t -> accessTokenManager + .getAuthenticationByToken(t.getToken())) + .orElse(Mono.empty())) + .flatMap(auth -> ReactiveLogger.mdc("userId", auth.getUser().getId()) + .then(ReactiveLogger.mdc("username", auth.getUser().getName())) + .thenReturn(auth)); + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeCache.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeCache.java new file mode 100644 index 000000000..0c7a3c4bb --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeCache.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.oauth2.server.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hswebframework.web.authorization.Authentication; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class AuthorizationCodeCache implements Serializable { + private static final long serialVersionUID = -6849794470754667710L; + + private String clientId; + + private String code; + + private Authentication authentication; + + private String scope; + +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeGranter.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeGranter.java new file mode 100644 index 000000000..ba0edbfec --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeGranter.java @@ -0,0 +1,31 @@ +package org.hswebframework.web.oauth2.server.code; + +import org.hswebframework.web.oauth2.server.AccessToken; +import org.hswebframework.web.oauth2.server.OAuth2Granter; +import reactor.core.publisher.Mono; + +/** + * 授权码模式认证 + * + * @author zhouhao + * @since 4.0.7 + */ +public interface AuthorizationCodeGranter extends OAuth2Granter { + + /** + * 申请授权码 + * + * @param request 请求 + * @return 授权码信息 + */ + Mono requestCode(AuthorizationCodeRequest request); + + /** + * 根据授权码获取token + * + * @param request 请求 + * @return token + */ + Mono requestToken(AuthorizationCodeTokenRequest request); + +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeRequest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeRequest.java new file mode 100644 index 000000000..0fe299319 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeRequest.java @@ -0,0 +1,27 @@ +package org.hswebframework.web.oauth2.server.code; + + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.OAuth2Request; + +import java.util.Map; + +@Getter +@Setter +public class AuthorizationCodeRequest extends OAuth2Request { + private OAuth2Client client; + + private Authentication authentication; + + + public AuthorizationCodeRequest(OAuth2Client client, + Authentication authentication, + Map parameters) { + super(parameters); + this.client = client; + this.authentication = authentication; + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeResponse.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeResponse.java new file mode 100644 index 000000000..da3cb769d --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeResponse.java @@ -0,0 +1,23 @@ +package org.hswebframework.web.oauth2.server.code; + + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.OAuth2Request; +import org.hswebframework.web.oauth2.server.OAuth2Response; + +import java.util.HashMap; + +@Getter +@Setter +@ToString +public class AuthorizationCodeResponse extends OAuth2Response { + private String code; + + public AuthorizationCodeResponse(String code) { + this.code = code; + with("code", code); + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeTokenRequest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeTokenRequest.java new file mode 100644 index 000000000..d3ba3a4fb --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/AuthorizationCodeTokenRequest.java @@ -0,0 +1,30 @@ +package org.hswebframework.web.oauth2.server.code; + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.OAuth2Request; + +import java.util.Map; +import java.util.Optional; + + +@Getter +@Setter +public class AuthorizationCodeTokenRequest extends OAuth2Request { + + private OAuth2Client client; + + public AuthorizationCodeTokenRequest(OAuth2Client client, Map parameters) { + super(parameters); + this.client = client; + } + + public Optional code() { + return getParameter("code").map(String::valueOf); + } + + public Optional scope() { + return getParameter("scope").map(String::valueOf); + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java new file mode 100644 index 000000000..a74c13be4 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranter.java @@ -0,0 +1,86 @@ +package org.hswebframework.web.oauth2.server.code; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.id.IDGenerator; +import org.hswebframework.web.oauth2.ErrorType; +import org.hswebframework.web.oauth2.OAuth2Exception; +import org.hswebframework.web.oauth2.server.AccessToken; +import org.hswebframework.web.oauth2.server.AccessTokenManager; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.ScopePredicate; +import org.hswebframework.web.oauth2.server.utils.OAuth2ScopeUtils; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisOperations; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +@AllArgsConstructor +public class DefaultAuthorizationCodeGranter implements AuthorizationCodeGranter { + + private final AccessTokenManager accessTokenManager; + + private final ReactiveRedisOperations redis; + + @SuppressWarnings("all") + public DefaultAuthorizationCodeGranter(AccessTokenManager accessTokenManager, ReactiveRedisConnectionFactory connectionFactory) { + this(accessTokenManager, new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext + .newSerializationContext() + .key((RedisSerializer) RedisSerializer.string()) + .value(RedisSerializer.java()) + .hashKey(RedisSerializer.string()) + .hashValue(RedisSerializer.java()) + .build() + )); + } + + @Override + public Mono requestCode(AuthorizationCodeRequest request) { + OAuth2Client client = request.getClient(); + Authentication authentication = request.getAuthentication(); + AuthorizationCodeCache codeCache = new AuthorizationCodeCache(); + String code = IDGenerator.MD5.generate(); + request.getParameter("scope").map(String::valueOf).ifPresent(codeCache::setScope); + codeCache.setCode(code); + codeCache.setClientId(client.getClientId()); + ScopePredicate permissionPredicate = OAuth2ScopeUtils.createScopePredicate(codeCache.getScope()); + + codeCache.setAuthentication(authentication.copy((permission, action) -> permissionPredicate.test(permission.getId(), action), dimension -> true)); + + + return redis + .opsForValue() + .set(getRedisKey(code), codeCache, Duration.ofMinutes(5)) + .thenReturn(new AuthorizationCodeResponse(code)); + } + + + private String getRedisKey(String code) { + return "oauth2-code:" + code; + } + + @Override + public Mono requestToken(AuthorizationCodeTokenRequest request) { + + return Mono + .justOrEmpty(request.code()) + .map(this::getRedisKey) + .flatMap(redis.opsForValue()::get) + .switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CODE))) + .flatMap(cache -> redis + .opsForValue() + .delete(getRedisKey(cache.getCode())) + .thenReturn(cache)) + .flatMap(cache -> { + if (!request.getClient().getClientId().equals(cache.getClientId())) { + return Mono.error(new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID)); + } + return accessTokenManager.createAccessToken(cache.getClientId(), cache.getAuthentication(), false); + }); + + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/CompositeOAuth2GrantService.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/CompositeOAuth2GrantService.java new file mode 100644 index 000000000..c4d3ec5ad --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/CompositeOAuth2GrantService.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.oauth2.server.impl; + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.oauth2.server.ClientCredentialGranter; +import org.hswebframework.web.oauth2.server.OAuth2GrantService; +import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter; + +@Getter +@Setter +public class CompositeOAuth2GrantService implements OAuth2GrantService { + + private AuthorizationCodeGranter authorizationCodeGranter; + + private ClientCredentialGranter clientCredentialGranter; + + @Override + public AuthorizationCodeGranter authorizationCode() { + return authorizationCodeGranter; + } + + @Override + public ClientCredentialGranter clientCredential() { + return clientCredentialGranter; + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java new file mode 100644 index 000000000..262116926 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessToken.java @@ -0,0 +1,37 @@ +package org.hswebframework.web.oauth2.server.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.oauth2.server.AccessToken; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class RedisAccessToken implements Serializable { + + private String clientId; + + private String accessToken; + + private String refreshToken; + + private long createTime; + + private Authentication authentication; + + private boolean singleton; + + public AccessToken toAccessToken(int expiresIn){ + AccessToken token=new AccessToken(); + token.setAccessToken(accessToken); + token.setRefreshToken(refreshToken); + token.setExpiresIn(expiresIn); + return token; + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java new file mode 100644 index 000000000..e6a0a0ac6 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManager.java @@ -0,0 +1,143 @@ +package org.hswebframework.web.oauth2.server.impl; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.codec.digest.DigestUtils; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.oauth2.ErrorType; +import org.hswebframework.web.oauth2.OAuth2Exception; +import org.hswebframework.web.oauth2.server.AccessToken; +import org.hswebframework.web.oauth2.server.AccessTokenManager; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisOperations; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.UUID; + +public class RedisAccessTokenManager implements AccessTokenManager { + + private final ReactiveRedisOperations tokenRedis; + + @Getter + @Setter + private int tokenExpireIn = 7200;//2小时 + + @Getter + @Setter + private int refreshExpireIn = 2592000; //30天 + + public RedisAccessTokenManager(ReactiveRedisOperations tokenRedis) { + this.tokenRedis = tokenRedis; + } + + @SuppressWarnings("all") + public RedisAccessTokenManager(ReactiveRedisConnectionFactory connectionFactory) { + this(new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext + .newSerializationContext() + .key((RedisSerializer) RedisSerializer.string()) + .value(RedisSerializer.java()) + .hashKey(RedisSerializer.string()) + .hashValue(RedisSerializer.java()) + .build() + )); + } + + @Override + public Mono getAuthenticationByToken(String accessToken) { + + return tokenRedis + .opsForValue() + .get(createTokenRedisKey(accessToken)) + .map(RedisAccessToken::getAuthentication); + } + + private String createTokenRedisKey(String token) { + return "oauth2-token:" + token; + } + + private String createRefreshTokenRedisKey(String token) { + return "oauth2-refresh-token:" + token; + } + + private String createSingletonTokenRedisKey(String clientId) { + return "oauth2-" + clientId + "-token"; + } + + private Mono doCreateAccessToken(String clientId, Authentication authentication, boolean singleton) { + String token = DigestUtils.md5Hex(UUID.randomUUID().toString()); + String refresh = DigestUtils.md5Hex(UUID.randomUUID().toString()); + RedisAccessToken accessToken = new RedisAccessToken(clientId, token, refresh, System.currentTimeMillis(), authentication, singleton); + + return storeToken(accessToken).thenReturn(accessToken); + } + + private Mono storeToken(RedisAccessToken token) { + return Mono + .zip( + tokenRedis.opsForValue().set(createTokenRedisKey(token.getAccessToken()), token, Duration.ofSeconds(tokenExpireIn)), + tokenRedis.opsForValue().set(createRefreshTokenRedisKey(token.getRefreshToken()), token, Duration.ofSeconds(refreshExpireIn)) + ).then(); + } + + private Mono doCreateSingletonAccessToken(String clientId, Authentication authentication) { + String redisKey = createSingletonTokenRedisKey(clientId); + + return tokenRedis + .opsForValue() + .get(redisKey) + .flatMap(token -> tokenRedis + .getExpire(redisKey) + .map(duration -> token.toAccessToken((int) (duration.toMillis() / 1000)))) + .switchIfEmpty(Mono.defer(() -> doCreateAccessToken(clientId, authentication, true) + .flatMap(redisAccessToken -> tokenRedis + .opsForValue() + .set(redisKey, redisAccessToken, Duration.ofSeconds(tokenExpireIn)) + .thenReturn(redisAccessToken.toAccessToken(tokenExpireIn)))) + ); + } + + @Override + public Mono createAccessToken(String clientId, + Authentication authentication, + boolean singleton) { + return singleton + ? doCreateSingletonAccessToken(clientId, authentication) + : doCreateAccessToken(clientId, authentication, false).map(token -> token.toAccessToken(tokenExpireIn)); + } + + @Override + public Mono refreshAccessToken(String clientId, String refreshToken) { + String redisKey = createRefreshTokenRedisKey(refreshToken); + + return tokenRedis + .opsForValue() + .get(redisKey) + .switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.EXPIRED_REFRESH_TOKEN))) + .flatMap(token -> { + if (!token.getClientId().equals(clientId)) { + return Mono.error(new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID)); + } + //生成新token + String accessToken = DigestUtils.md5Hex(UUID.randomUUID().toString()); + token.setAccessToken(accessToken); + token.setCreateTime(System.currentTimeMillis()); + return storeToken(token) + .as(result -> { + // 单例token + if (token.isSingleton()) { + return tokenRedis + .opsForValue() + .set(createSingletonTokenRedisKey(clientId), token, Duration.ofSeconds(tokenExpireIn)) + .then(result); + } + return result; + }) + .thenReturn(token.toAccessToken(tokenExpireIn)); + }); + + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java new file mode 100644 index 000000000..4ac30fff0 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtils.java @@ -0,0 +1,32 @@ +package org.hswebframework.web.oauth2.server.utils; + +import org.hswebframework.web.oauth2.server.ScopePredicate; +import org.springframework.util.StringUtils; + +import java.util.*; + +/** + * @author zhouhao + * @since 4.0.8 + */ +public class OAuth2ScopeUtils { + + public static ScopePredicate createScopePredicate(String scopeStr) { + if (StringUtils.isEmpty(scopeStr)) { + return ((permission, action) -> false); + } + String[] scopes = scopeStr.split("[ ,\n]"); + Map> actions = new HashMap<>(); + for (String scope : scopes) { + String[] permissions = scope.split("[:]"); + String per = permissions[0]; + Set acts = actions.computeIfAbsent(per, k -> new HashSet<>()); + acts.addAll(Arrays.asList(permissions).subList(1, permissions.length)); + } + + return ((permission, action) -> Optional + .ofNullable(actions.get(permission)) + .map(acts -> action.length == 0 || acts.containsAll(Arrays.asList(action))) + .orElse(false)); + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java new file mode 100644 index 000000000..dc30e1dd7 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeController.java @@ -0,0 +1,115 @@ +package org.hswebframework.web.oauth2.server.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.annotation.Authorize; +import org.hswebframework.web.authorization.exception.UnAuthorizedException; +import org.hswebframework.web.oauth2.ErrorType; +import org.hswebframework.web.oauth2.OAuth2Exception; +import org.hswebframework.web.oauth2.server.AccessToken; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.OAuth2ClientManager; +import org.hswebframework.web.oauth2.server.OAuth2GrantService; +import org.hswebframework.web.oauth2.server.code.AuthorizationCodeRequest; +import org.hswebframework.web.oauth2.server.code.AuthorizationCodeTokenRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/oauth2") +@AllArgsConstructor +@Tag(name = "OAuth2认证") +public class OAuth2AuthorizeController { + + private final OAuth2GrantService oAuth2GrantService; + + private final OAuth2ClientManager clientManager; + + + @GetMapping(value = "/authorize", params = "response_type=code") + @Operation(summary = "申请授权码,并获取重定向地址", parameters = { + @Parameter(name = "client_id", required = true), + @Parameter(name = "redirect_uri", required = true), + @Parameter(name = "state"), + @Parameter(name = "response_type", description = "固定值为code") + }) + public Mono authorizeByCode(ServerWebExchange exchange) { + Map param = new HashMap<>(exchange.getRequest().getQueryParams().toSingleValueMap()); + + return Authentication + .currentReactive() + .switchIfEmpty(Mono.error(UnAuthorizedException::new)) + .flatMap(auth -> this + .getOAuth2Client((String) param.get("client_id")) + .switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID))) + .flatMap(client -> { + String redirectUri = (String) param.getOrDefault("redirect_uri", client.getRedirectUrl()); + client.validateRedirectUri(redirectUri); + return oAuth2GrantService + .authorizationCode() + .requestCode(new AuthorizationCodeRequest(client, auth, param)) + .doOnNext(response -> { + Optional.ofNullable(param.get("state")).ifPresent(state -> response.with("state", state)); + }) + .map(response -> buildRedirect(redirectUri, response.getParameters())); + })); + } + + @GetMapping(value = "/token", params = "grant_type=authorization_code") + @Operation(summary = "使用授权码申请token", parameters = { + @Parameter(name = "client_id"), + @Parameter(name = "client_secret"), + @Parameter(name = "code"), + @Parameter(name = "grant_type", description = "固定值为authorization_code") + }) + @Authorize(ignore = true) + public Mono> requestTokenByCode(ServerWebExchange exchange) { + Map params = exchange.getRequest().getQueryParams().toSingleValueMap(); + + return doRequestCode(new HashMap<>(params)) + .map(ResponseEntity::ok); + } + + private Mono doRequestCode(Map param) { + return this + .getOAuth2Client((String) param.get("client_id")) + .switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID))) + .flatMap(client -> oAuth2GrantService + .authorizationCode() + .requestToken(new AuthorizationCodeTokenRequest(client, param))); + } + + + @SneakyThrows + public static String urlEncode(String url) { + return URLEncoder.encode(url, "utf-8"); + } + + static String buildRedirect(String redirectUri, Map params) { + String paramsString = params.entrySet() + .stream() + .map(e -> e.getKey() + "=" + urlEncode(String.valueOf(e.getValue()))) + .collect(Collectors.joining("&")); + if (redirectUri.contains("?")) { + return redirectUri + "&" + paramsString; + } + return redirectUri + "?" + paramsString; + } + + + private Mono getOAuth2Client(String id) { + return clientManager.getClient(id); + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring.factories b/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..8e76d8af6 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.hswebframework.web.oauth2.server.OAuth2ServerAutoConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/OAuth2ClientTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/OAuth2ClientTest.java new file mode 100644 index 000000000..698c4040b --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/OAuth2ClientTest.java @@ -0,0 +1,20 @@ +package org.hswebframework.web.oauth2.server; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class OAuth2ClientTest { + + @Test + public void test(){ + OAuth2Client client=new OAuth2Client(); + + client.setRedirectUrl("http://hsweb.me/callback"); + + client.validateRedirectUri("http://hsweb.me/callback"); + + client.validateRedirectUri("http://hsweb.me/callback?a=1&n=1"); + + } +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/RedisHelper.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/RedisHelper.java new file mode 100644 index 000000000..c5237bc20 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/RedisHelper.java @@ -0,0 +1,15 @@ +package org.hswebframework.web.oauth2.server; + +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +public class RedisHelper { + + public static LettuceConnectionFactory factory; + + static { + factory = new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1")); + factory.afterPropertiesSet(); + } +} diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java new file mode 100644 index 000000000..4f9fdaacc --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/code/DefaultAuthorizationCodeGranterTest.java @@ -0,0 +1,42 @@ +package org.hswebframework.web.oauth2.server.code; + +import org.hswebframework.web.authorization.Permission; +import org.hswebframework.web.authorization.simple.SimpleAuthentication; +import org.hswebframework.web.authorization.simple.SimplePermission; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.RedisHelper; +import org.hswebframework.web.oauth2.server.impl.RedisAccessTokenManager; +import org.junit.Test; +import reactor.test.StepVerifier; + +import java.util.Collections; +import java.util.function.BiPredicate; + +import static org.junit.Assert.*; + +public class DefaultAuthorizationCodeGranterTest { + + @Test + public void testRequestToken() { + + DefaultAuthorizationCodeGranter codeGranter = new DefaultAuthorizationCodeGranter( + new RedisAccessTokenManager(RedisHelper.factory), RedisHelper.factory + ); + + OAuth2Client client = new OAuth2Client(); + client.setClientId("test"); + client.setClientSecret("test"); + + codeGranter + .requestCode(new AuthorizationCodeRequest(client, new SimpleAuthentication(), Collections.emptyMap())) + .doOnNext(System.out::println) + .flatMap(response -> codeGranter + .requestToken(new AuthorizationCodeTokenRequest(client, Collections.singletonMap("code", response.getCode())))) + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + } + +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java new file mode 100644 index 000000000..6be2ee561 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/impl/RedisAccessTokenManagerTest.java @@ -0,0 +1,45 @@ +package org.hswebframework.web.oauth2.server.impl; + +import org.hswebframework.web.authorization.simple.SimpleAuthentication; +import org.hswebframework.web.oauth2.server.RedisHelper; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.Assert.*; + +public class RedisAccessTokenManagerTest { + + @Test + public void testCreateAccessToken() { + RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory); + + SimpleAuthentication authentication = new SimpleAuthentication(); + + tokenManager.createAccessToken("test", authentication, false) + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + } + + @Test + public void testCreateSingletonAccessToken() { + RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory); + + SimpleAuthentication authentication = new SimpleAuthentication(); + + Flux + .concat(tokenManager + .createAccessToken("test", authentication, true), + tokenManager + .createAccessToken("test", authentication, true)) + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextCount(2) + .verifyComplete(); + + } +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtilsTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtilsTest.java new file mode 100644 index 000000000..83cb9787a --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/utils/OAuth2ScopeUtilsTest.java @@ -0,0 +1,35 @@ +package org.hswebframework.web.oauth2.server.utils; + +import org.hswebframework.web.oauth2.server.ScopePredicate; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OAuth2ScopeUtilsTest { + + + @Test + public void testEmpty() { + ScopePredicate predicate = OAuth2ScopeUtils.createScopePredicate(null); + assertFalse(predicate.test("basic")); + } + + @Test + public void testScope() { + ScopePredicate predicate = OAuth2ScopeUtils.createScopePredicate("basic user:info device:query"); + + assertTrue(predicate.test("basic")); + { + + assertTrue(predicate.test("user", "info")); + assertFalse(predicate.test("user", "info2")); + } + + { + assertTrue(predicate.test("device", "query")); + assertFalse(predicate.test("device", "query2")); + } + + } +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeControllerTest.java b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeControllerTest.java new file mode 100644 index 000000000..5778117a3 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/server/web/OAuth2AuthorizeControllerTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.web.oauth2.server.web; + +import org.junit.Test; + +import java.util.Collections; + +import static org.junit.Assert.*; + +public class OAuth2AuthorizeControllerTest { + + @Test + public void testBuildRedirect() { + String url = OAuth2AuthorizeController.buildRedirect("http://hsweb.me/callback", Collections.singletonMap("code", "1234")); + + assertEquals(url,"http://hsweb.me/callback?code=1234"); + } + + @Test + public void testBuildRedirectParam() { + String url = OAuth2AuthorizeController.buildRedirect("http://hsweb.me/callback?a=b", Collections.singletonMap("code", "1234")); + + assertEquals(url,"http://hsweb.me/callback?a=b&code=1234"); + } + +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/test/resources/logback.xml b/hsweb-authorization/hsweb-authorization-oauth2/src/test/resources/logback.xml new file mode 100644 index 000000000..fbdf2f230 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-oauth2/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + +   + +   +   + + + %-4relative [%thread] %-5level %logger{35} - %msg %n + + + + + + + \ No newline at end of file diff --git a/hsweb-authorization/pom.xml b/hsweb-authorization/pom.xml index b99945ae5..f690d3f7e 100644 --- a/hsweb-authorization/pom.xml +++ b/hsweb-authorization/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 @@ -14,6 +14,7 @@ hsweb-authorization-api hsweb-authorization-basic + hsweb-authorization-oauth2 diff --git a/hsweb-commons/hsweb-commons-api/pom.xml b/hsweb-commons/hsweb-commons-api/pom.xml index fb4254130..33ea46949 100644 --- a/hsweb-commons/hsweb-commons-api/pom.xml +++ b/hsweb-commons/hsweb-commons-api/pom.xml @@ -5,7 +5,7 @@ hsweb-commons org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-commons/hsweb-commons-crud/pom.xml b/hsweb-commons/hsweb-commons-crud/pom.xml index 33c95299f..c708b2dc3 100644 --- a/hsweb-commons/hsweb-commons-crud/pom.xml +++ b/hsweb-commons/hsweb-commons-crud/pom.xml @@ -5,7 +5,7 @@ hsweb-commons org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java index 5eb20f24e..dfa6caeae 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java @@ -1,6 +1,5 @@ package org.hswebframework.web.crud.service; -import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.utils.RandomUtil; import org.hswebframework.web.api.crud.entity.QueryParamEntity; @@ -13,6 +12,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -30,13 +31,17 @@ public interface ReactiveTreeSortEntityService> queryResultToTree(QueryParamEntity paramEntity) { return query(paramEntity) .collectList() - .map(list -> TreeSupportEntity.list2tree(list, this::setChildren, this::isRootNode)); + .map(list -> TreeSupportEntity.list2tree(list, + this::setChildren, + this::createRootNodePredicate)); } default Mono> queryIncludeChildrenTree(QueryParamEntity paramEntity) { return queryIncludeChildren(paramEntity) .collectList() - .map(list -> TreeSupportEntity.list2tree(list, this::setChildren, this::isRootNode)); + .map(list -> TreeSupportEntity.list2tree(list, + this::setChildren, + this::createRootNodePredicate)); } default Flux queryIncludeChildren(Collection idList) { @@ -115,6 +120,19 @@ public interface ReactiveTreeSortEntityService createRootNodePredicate(TreeSupportEntity.TreeHelper helper) { + return node -> { + if (isRootNode(node)) { + return true; + } + //有父节点,但是父节点不存在 + if (!StringUtils.isEmpty(node.getParentId())) { + return helper.getNode(node.getParentId()) == null; + } + return false; + }; + } + default boolean isRootNode(E entity) { return StringUtils.isEmpty(entity.getParentId()) || "-1".equals(String.valueOf(entity.getParentId())); } diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeSortEntityService.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeSortEntityService.java index f878d8d8b..7b187ffd5 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeSortEntityService.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestTreeSortEntityService.java @@ -25,9 +25,5 @@ public class TestTreeSortEntityService extends GenericReactiveCrudService hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml 4.0.0 diff --git a/hsweb-concurrent/hsweb-concurrent-cache/pom.xml b/hsweb-concurrent/hsweb-concurrent-cache/pom.xml index 1292f0de8..955503c59 100644 --- a/hsweb-concurrent/hsweb-concurrent-cache/pom.xml +++ b/hsweb-concurrent/hsweb-concurrent-cache/pom.xml @@ -5,7 +5,7 @@ hsweb-concurrent org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-concurrent/pom.xml b/hsweb-concurrent/pom.xml index 710ae65f9..31f6e26d4 100644 --- a/hsweb-concurrent/pom.xml +++ b/hsweb-concurrent/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-core/pom.xml b/hsweb-core/pom.xml index 8e48cbf53..f67173cad 100644 --- a/hsweb-core/pom.xml +++ b/hsweb-core/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml 4.0.0 diff --git a/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java index a0f725828..6640aa5f9 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/context/ContextUtils.java @@ -11,8 +11,7 @@ import java.util.function.Function; */ public class ContextUtils { - private static ThreadLocal contextThreadLocal = ThreadLocal.withInitial(MapContext::new); - + private static final ThreadLocal contextThreadLocal = ThreadLocal.withInitial(MapContext::new); public static Context currentContext() { return contextThreadLocal.get(); @@ -23,6 +22,8 @@ public class ContextUtils { .handle((context, sink) -> { if (context.hasKey(Context.class)) { sink.next(context.get(Context.class)); + }else { + sink.complete(); } }) .subscriberContext(acceptContext(ctx -> { diff --git a/hsweb-datasource/hsweb-datasource-api/pom.xml b/hsweb-datasource/hsweb-datasource-api/pom.xml index d977fe014..28140b455 100644 --- a/hsweb-datasource/hsweb-datasource-api/pom.xml +++ b/hsweb-datasource/hsweb-datasource-api/pom.xml @@ -5,7 +5,7 @@ hsweb-datasource org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml diff --git a/hsweb-datasource/hsweb-datasource-jta/pom.xml b/hsweb-datasource/hsweb-datasource-jta/pom.xml index ff82dba67..54aef440a 100644 --- a/hsweb-datasource/hsweb-datasource-jta/pom.xml +++ b/hsweb-datasource/hsweb-datasource-jta/pom.xml @@ -5,7 +5,7 @@ hsweb-datasource org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml diff --git a/hsweb-datasource/hsweb-datasource-web/pom.xml b/hsweb-datasource/hsweb-datasource-web/pom.xml index 00715ee53..d796f9d7b 100644 --- a/hsweb-datasource/hsweb-datasource-web/pom.xml +++ b/hsweb-datasource/hsweb-datasource-web/pom.xml @@ -5,7 +5,7 @@ hsweb-datasource org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml diff --git a/hsweb-datasource/pom.xml b/hsweb-datasource/pom.xml index a68a3d3dc..29b736456 100644 --- a/hsweb-datasource/pom.xml +++ b/hsweb-datasource/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml diff --git a/hsweb-logging/hsweb-access-logging-aop/pom.xml b/hsweb-logging/hsweb-access-logging-aop/pom.xml index 0114e92d9..41ee6767f 100644 --- a/hsweb-logging/hsweb-access-logging-aop/pom.xml +++ b/hsweb-logging/hsweb-access-logging-aop/pom.xml @@ -5,7 +5,7 @@ hsweb-logging org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml 4.0.0 diff --git a/hsweb-logging/hsweb-access-logging-api/pom.xml b/hsweb-logging/hsweb-access-logging-api/pom.xml index a7ee892b5..8ebef4191 100644 --- a/hsweb-logging/hsweb-access-logging-api/pom.xml +++ b/hsweb-logging/hsweb-access-logging-api/pom.xml @@ -5,7 +5,7 @@ hsweb-logging org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml 4.0.0 diff --git a/hsweb-logging/pom.xml b/hsweb-logging/pom.xml index c5307e15d..6b693ad19 100644 --- a/hsweb-logging/pom.xml +++ b/hsweb-logging/pom.xml @@ -23,7 +23,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml 4.0.0 diff --git a/hsweb-starter/pom.xml b/hsweb-starter/pom.xml index de2208e28..6fe262381 100644 --- a/hsweb-starter/pom.xml +++ b/hsweb-starter/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml index 3a88edf23..09895d9e3 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/pom.xml @@ -5,7 +5,7 @@ hsweb-system-authorization org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml index a17997f99..6df4ac6a4 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml @@ -5,7 +5,7 @@ hsweb-system-authorization org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml new file mode 100644 index 000000000..10c2d37fa --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/pom.xml @@ -0,0 +1,84 @@ + + + + hsweb-system-authorization + org.hswebframework.web + 4.0.8-SNAPSHOT + ../pom.xml + + 4.0.0 + + hsweb-system-authorization-oauth2 + + + + org.hswebframework.web + hsweb-commons-crud + ${project.version} + + + + org.hswebframework.web + hsweb-authorization-oauth2 + ${project.version} + + + + org.springframework + spring-aspects + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-jdbc + test + + + + com.h2database + h2 + test + + + + org.springframework.boot + spring-boot-starter-data-r2dbc + test + + + + io.r2dbc + r2dbc-h2 + test + + + + org.hswebframework.web + hsweb-authorization-api + ${project.version} + compile + + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + test + + + + org.springframework.boot + spring-boot-starter-webflux + test + + + + \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java new file mode 100644 index 000000000..c9121c58f --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfiguration.java @@ -0,0 +1,37 @@ +package org.hswebframework.web.oauth2.configuration; + +import org.hswebframework.web.oauth2.server.OAuth2ClientManager; +import org.hswebframework.web.oauth2.service.InDBOAuth2ClientManager; +import org.hswebframework.web.oauth2.service.OAuth2ClientService; +import org.hswebframework.web.oauth2.web.WebFluxOAuth2ClientController; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class OAuth2ClientManagerAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + static class ReactiveOAuth2ClientManagerAutoConfiguration { + + @Bean + public OAuth2ClientService oAuth2ClientService() { + return new OAuth2ClientService(); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2ClientManager oAuth2ClientManager(OAuth2ClientService clientService) { + return new InDBOAuth2ClientManager(clientService); + } + + @Bean + @ConditionalOnMissingBean + public WebFluxOAuth2ClientController webFluxOAuth2ClientController(OAuth2ClientService clientService){ + return new WebFluxOAuth2ClientController(clientService); + } + } + +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/entity/OAuth2ClientEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/entity/OAuth2ClientEntity.java new file mode 100644 index 000000000..88274fc54 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/entity/OAuth2ClientEntity.java @@ -0,0 +1,84 @@ +package org.hswebframework.web.oauth2.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; +import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; +import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.bean.ToString; +import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.oauth2.enums.OAuth2ClientState; +import org.hswebframework.web.oauth2.server.OAuth2Client; + +import javax.persistence.Column; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; + +@Table(name = "s_oauth2_client") +@Getter +@Setter +public class OAuth2ClientEntity extends GenericEntity { + + @Column(length = 1024) + @Schema(description = "Logo地址") + private String logoUrl; + + @Column(length = 64, nullable = false) + @Schema(description = "客户端名称") + @NotBlank + private String name; + + @Column(length = 128, nullable = false) + @Schema(description = "密钥") + @NotBlank + @ToString.Ignore + private String secret; + + @Column(length = 64, nullable = false) + @Schema(description = "绑定用户ID") + @NotBlank + private String userId; + + @Column(length = 1024, nullable = false) + @Schema(description = "回调地址") + @NotBlank + private String callbackUri; + + @Column(length = 1024, nullable = false) + @Schema(description = "首页地址") + @NotBlank + private String homeUri; + + @Column + @Schema(description = "说明") + private String description; + + @Column(length = 32) + @EnumCodec + @ColumnType(javaType = String.class) + @DefaultValue("enabled") + @Schema(description = "状态") + private OAuth2ClientState state; + + @Column(nullable = false) + @Schema(description = "创建时间") + @DefaultValue(generator = Generators.CURRENT_TIME) + private Long createTime; + + public boolean enabled() { + return state == OAuth2ClientState.enabled; + } + + public OAuth2Client toOAuth2Client() { + OAuth2Client client = new OAuth2Client(); + client.setClientSecret(secret); + client.setClientId(getId()); + client.setName(getName()); + client.setRedirectUrl(callbackUri); + client.setDescription(description); + client.setUserId(userId); + return client; + } +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/enums/OAuth2ClientState.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/enums/OAuth2ClientState.java new file mode 100644 index 000000000..1de35d578 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/enums/OAuth2ClientState.java @@ -0,0 +1,20 @@ +package org.hswebframework.web.oauth2.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.EnumDict; + +@Getter +@AllArgsConstructor +public enum OAuth2ClientState implements EnumDict { + + enabled("启用"), + disabled("禁用"); + private final String text; + + @Override + public String getValue() { + return name(); + } + +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/service/InDBOAuth2ClientManager.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/service/InDBOAuth2ClientManager.java new file mode 100644 index 000000000..a6d9c33a4 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/service/InDBOAuth2ClientManager.java @@ -0,0 +1,21 @@ +package org.hswebframework.web.oauth2.service; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity; +import org.hswebframework.web.oauth2.server.OAuth2Client; +import org.hswebframework.web.oauth2.server.OAuth2ClientManager; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class InDBOAuth2ClientManager implements OAuth2ClientManager { + + private final OAuth2ClientService clientService; + + @Override + public Mono getClient(String clientId) { + return clientService + .findById(clientId) + .filter(OAuth2ClientEntity::enabled) + .map(OAuth2ClientEntity::toOAuth2Client); + } +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/service/OAuth2ClientService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/service/OAuth2ClientService.java new file mode 100644 index 000000000..77b5c2cb5 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/service/OAuth2ClientService.java @@ -0,0 +1,12 @@ +package org.hswebframework.web.oauth2.service; + +import org.hswebframework.web.crud.service.GenericReactiveCacheSupportCrudService; +import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity; + +public class OAuth2ClientService extends GenericReactiveCacheSupportCrudService { + + @Override + public String getCacheName() { + return "oauth2-client"; + } +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/web/WebFluxOAuth2ClientController.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/web/WebFluxOAuth2ClientController.java new file mode 100644 index 000000000..ac24b9b00 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/web/WebFluxOAuth2ClientController.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.oauth2.web; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.annotation.Resource; +import org.hswebframework.web.crud.service.ReactiveCrudService; +import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; +import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity; +import org.hswebframework.web.oauth2.service.OAuth2ClientService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/oauth2/client") +@AllArgsConstructor +@Resource(id = "oauth2-client", name = "OAuth2客户端管理") +@Tag(name = "OAuth2客户端管理") +public class WebFluxOAuth2ClientController implements ReactiveServiceCrudController { + + private final OAuth2ClientService oAuth2ClientService; + + @Override + public ReactiveCrudService getService() { + return oAuth2ClientService; + } +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring.factories b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..4f804d6fb --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.hswebframework.web.oauth2.configuration.OAuth2ClientManagerAutoConfiguration \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/ReactiveTestApplication.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/ReactiveTestApplication.java new file mode 100644 index 000000000..6a8238962 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/ReactiveTestApplication.java @@ -0,0 +1,22 @@ +package org.hswebframework.web.oauth2; + +import org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration; +import org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; + +@SpringBootApplication(exclude = { + //TransactionAutoConfiguration.class, + JdbcSqlExecutorConfiguration.class, + DataSourceAutoConfiguration.class +}) +@ImportAutoConfiguration({ + R2dbcTransactionManagerAutoConfiguration.class, + DefaultAuthorizationAutoConfiguration.class +}) +public class ReactiveTestApplication { + + +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfigurationTest.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfigurationTest.java new file mode 100644 index 000000000..61284aa8d --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/configuration/OAuth2ClientManagerAutoConfigurationTest.java @@ -0,0 +1,24 @@ +package org.hswebframework.web.oauth2.configuration; + +import org.hswebframework.web.oauth2.ReactiveTestApplication; +import org.hswebframework.web.oauth2.server.OAuth2ClientManager; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ReactiveTestApplication.class) +public class OAuth2ClientManagerAutoConfigurationTest { + + @Autowired + OAuth2ClientManager clientManager; + + @Test + public void test(){ + assertNotNull(clientManager); + } +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/service/OAuth2ClientServiceTest.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/service/OAuth2ClientServiceTest.java new file mode 100644 index 000000000..f5ac463e7 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/java/org/hswebframework/web/oauth2/service/OAuth2ClientServiceTest.java @@ -0,0 +1,46 @@ +package org.hswebframework.web.oauth2.service; + +import org.hswebframework.web.oauth2.ReactiveTestApplication; +import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ReactiveTestApplication.class) +public class OAuth2ClientServiceTest { + + @Autowired + OAuth2ClientService clientService; + + @Test + public void test() { + + OAuth2ClientEntity clientEntity = new OAuth2ClientEntity(); + clientEntity.setId("test"); + clientEntity.setHomeUri("http://hsweb.me"); + clientEntity.setCallbackUri("http://hsweb.me/callback"); + clientEntity.setSecret("test"); + clientEntity.setName("test"); + clientEntity.setUserId("admin"); + clientService.insert(Mono.just(clientEntity)) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + clientService.findById("test") + .doOnNext(System.out::println) + .as(StepVerifier::create) + .expectNextMatches(client -> { + return client.getCreateTime() != null && client.getState() != null; + }).verifyComplete(); + + } + +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/resources/application.yml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/resources/application.yml new file mode 100644 index 000000000..dcb4516fd --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-oauth2/src/test/resources/application.yml @@ -0,0 +1,16 @@ +logging: + level: + org.hswebframework: debug + org.springframework.transaction: debug + org.springframework.data.r2dbc.connectionfactory: debug +#spring: +# r2dbc: +spring: + aop: + proxy-target-class: true +hsweb: + authorize: + auto-parse: true +easyorm: + default-schema: PUBLIC + dialect: h2 \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/pom.xml b/hsweb-system/hsweb-system-authorization/pom.xml index d91bb2abd..c6cb80530 100644 --- a/hsweb-system/hsweb-system-authorization/pom.xml +++ b/hsweb-system/hsweb-system-authorization/pom.xml @@ -5,7 +5,7 @@ hsweb-system org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 pom @@ -14,9 +14,9 @@ hsweb-system-authorization-api hsweb-system-authorization-default + hsweb-system-authorization-oauth2 hsweb-system-authorization - \ No newline at end of file diff --git a/hsweb-system/hsweb-system-dictionary/pom.xml b/hsweb-system/hsweb-system-dictionary/pom.xml index 29a1e338f..4f788d4f9 100644 --- a/hsweb-system/hsweb-system-dictionary/pom.xml +++ b/hsweb-system/hsweb-system-dictionary/pom.xml @@ -5,7 +5,7 @@ hsweb-system org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-system/hsweb-system-file/pom.xml b/hsweb-system/hsweb-system-file/pom.xml index c2e23eee3..35bb930aa 100644 --- a/hsweb-system/hsweb-system-file/pom.xml +++ b/hsweb-system/hsweb-system-file/pom.xml @@ -5,7 +5,7 @@ hsweb-system org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT 4.0.0 diff --git a/hsweb-system/pom.xml b/hsweb-system/pom.xml index 5f2f8aa0a..25abbfb03 100644 --- a/hsweb-system/pom.xml +++ b/hsweb-system/pom.xml @@ -5,7 +5,7 @@ hsweb-framework org.hswebframework.web - 4.0.7 + 4.0.8-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index d0039e27f..87350d05a 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.hswebframework.web hsweb-framework - 4.0.7 + 4.0.8-SNAPSHOT hsweb-starter hsweb-core @@ -84,13 +84,13 @@ 3.20.0-GA 5.19.0.2 - 1.2.47 + 1.2.74 1.4.200 5.1.39 3.2.2 1.6.12 - 4.0.7 + 4.0.6-SNAPSHOT 3.0.2 3.0.2 2.7.0 @@ -301,7 +301,6 @@ ch.qos.logback logback-classic - 1.1.7 test @@ -360,7 +359,7 @@ junit junit - 4.12 + 4.13.1 test @@ -391,7 +390,7 @@ commons-beanutils commons-beanutils - 1.9.3 + 1.9.4