Merge branch 'master' of github.com:hs-web/hsweb-framework

This commit is contained in:
zhangji
2023-03-22 18:48:09 +08:00
88 changed files with 1355 additions and 450 deletions

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -2,11 +2,14 @@ package org.hswebframework.web.authorization.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.event.DefaultAsyncEvent;
@Getter
@Setter
@AllArgsConstructor
public class AuthorizationInitializeEvent {
public class AuthorizationInitializeEvent extends DefaultAsyncEvent {
private Authentication authentication;
}

View File

@@ -1,6 +1,7 @@
package org.hswebframework.web.authorization.simple;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.hswebframework.web.authorization.*;
import reactor.core.publisher.Flux;
@@ -11,6 +12,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
@AllArgsConstructor
@Slf4j
public class CompositeReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private final List<ReactiveAuthenticationManagerProvider> providers;
@@ -21,7 +23,10 @@ public class CompositeReactiveAuthenticationManager implements ReactiveAuthentic
.stream()
.map(manager -> manager
.authenticate(request)
.onErrorResume((err) -> Mono.empty()))
.onErrorResume((err) -> {
log.warn("get user authenticate error", err);
return Mono.empty();
}))
.collect(Collectors.toList()))
.take(1)
.next();
@@ -34,7 +39,10 @@ public class CompositeReactiveAuthenticationManager implements ReactiveAuthentic
.stream()
.map(manager -> manager
.getByUserId(userId)
.onErrorResume((err) -> Mono.empty())
.onErrorResume((err) -> {
log.warn("get user [{}] authentication error", userId, err);
return Mono.empty();
})
))
.flatMap(Function.identity())
.collectList()

View File

@@ -58,10 +58,16 @@ public class SimpleAuthentication implements Authentication {
}
public SimpleAuthentication merge(Authentication authentication) {
Map<String, Permission> mePermissionGroup = permissions.stream()
Map<String, Permission> mePermissionGroup = permissions
.stream()
.collect(Collectors.toMap(Permission::getId, Function.identity()));
user = authentication.getUser();
if (authentication.getUser() != null) {
user = authentication.getUser();
}
attributes.putAll(authentication.getAttributes());
for (Permission permission : authentication.getPermissions()) {
Permission me = mePermissionGroup.get(permission.getId());
if (me == null) {
@@ -72,7 +78,6 @@ public class SimpleAuthentication implements Authentication {
me.getDataAccesses().addAll(permission.getDataAccesses());
}
for (Dimension dimension : authentication.getDimensions()) {
if (!getDimension(dimension.getType(), dimension.getId()).isPresent()) {
dimensions.add(dimension);
@@ -85,14 +90,27 @@ public class SimpleAuthentication implements Authentication {
public Authentication copy(BiPredicate<Permission, String> permissionFilter,
Predicate<Dimension> 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())
.stream()
.map(permission -> permission.copy(action -> permissionFilter.test(permission, action), conf -> true))
.filter(per -> !per.getActions().isEmpty())
.collect(Collectors.toList())
);
authentication.setUser(user);
return authentication;
}
public void setUser(User user) {
this.user = user;
dimensions.add(user);
}
public void setDimensions(List<Dimension> dimensions) {
this.dimensions.addAll(dimensions);
}
public void addDimension(Dimension dimension) {
this.dimensions.add(dimension);
}
}

View File

@@ -53,9 +53,13 @@ public class RedisUserTokenManager implements UserTokenManager {
.buffer(Flux.interval(Duration.ofSeconds(10)), HashSet::new)
.flatMap(list -> Flux
.fromIterable(list)
.flatMap(token -> operations
.expire(getTokenRedisKey(token.getToken()), Duration.ofMillis(token.getMaxInactiveInterval()))
.then())
.flatMap(token -> {
String key = getTokenRedisKey(token.getToken());
return Mono
.zip(this.userTokenStore.put(key, "lastRequestTime", token.getLastRequestTime()),
this.operations.expire(key, Duration.ofMillis(token.getMaxInactiveInterval())))
.then();
})
.onErrorResume(err -> Mono.empty()))
.subscribe();
@@ -103,7 +107,7 @@ public class RedisUserTokenManager implements UserTokenManager {
return userTokenStore
.entries(getTokenRedisKey(token))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
.filter(map -> !map.isEmpty())
.filter(map -> !map.isEmpty() && map.containsKey("token") && map.containsKey("userId"))
.map(SimpleUserToken::of)
.doOnNext(userToken -> localCache.put(userToken.getToken(), userToken))
.cast(UserToken.class);

View File

@@ -2,6 +2,7 @@ error.access_denied=Access Denied
error.permission_denied=Permission Denied [{0}]:{1}
error.logged_in_elsewhere=User logged in elsewhere
error.illegal_password=Bad username or password
error.illegal_user_password=Bad Password
error.user_disabled=User is disabled
#
message.token_state_normal=Normal

View File

@@ -2,6 +2,7 @@ error.access_denied=暂无权限,请联系管理员!
error.permission_denied=当前用户无权限[{0}]:{1}
error.logged_in_elsewhere=该用户已在其他地方登陆
error.illegal_password=用户名或密码错误
error.illegal_user_password=密码错误
error.user_disabled=用户已被禁用
#
message.token_state_normal=正常

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -64,7 +64,8 @@ public class AopAuthorizingController extends StaticMethodMatcherPointcutAdvisor
AuthorizingContext context,
Supplier<? extends Publisher<?>> invoker) {
return Authentication.currentReactive()
return Authentication
.currentReactive()
.switchIfEmpty(Mono.error(UnAuthorizedException::new))
.flatMapMany(auth -> {
context.setAuthentication(auth);
@@ -133,8 +134,7 @@ public class AopAuthorizingController extends StaticMethodMatcherPointcutAdvisor
context.setAuthentication(authentication);
isControl = true;
Phased dataAccessPhased = null;
dataAccessPhased = definition.getResources().getPhased();
Phased dataAccessPhased = definition.getResources().getPhased();
if (definition.getPhased() == Phased.before) {
//RDAC before
authorizingHandler.handRBAC(context);

View File

@@ -145,6 +145,12 @@ public class AuthorizingHandlerAutoConfiguration {
return new ReactiveUserTokenController();
}
@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public BearerTokenParser bearerTokenParser() {
return new BearerTokenParser();
}
@Configuration
public static class DataAccessHandlerProcessor implements BeanPostProcessor {

View File

@@ -38,7 +38,7 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
private String message = "error.access_denied";
private Phased phased;
private Phased phased = Phased.before;
@Override
public boolean isEmpty() {
@@ -65,6 +65,7 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
getResources().getResources().clear();
getDimensions().getDimensions().clear();
}
setPhased(ann.phased());
getResources().setPhased(ann.phased());
for (Resource resource : ann.resources()) {
putAnnotation(resource);
@@ -97,6 +98,8 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
putAnnotation(resource, action);
}
resource.setGroup(new ArrayList<>(Arrays.asList(ann.group())));
setPhased(ann.phased());
getResources().setPhased(ann.phased());
resources.addResource(resource, ann.merge());
}
@@ -132,8 +135,8 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
return;
}
definition.getDataAccess()
.getDataAccessTypes()
.add(typeDefinition);
.getDataAccessTypes()
.add(typeDefinition);
}
public void putAnnotation(ResourceActionDefinition definition, DataAccessType dataAccessType) {
@@ -147,8 +150,8 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
typeDefinition.setConfiguration(dataAccessType.configuration());
typeDefinition.setDescription(String.join("\n", dataAccessType.description()));
definition.getDataAccess()
.getDataAccessTypes()
.add(typeDefinition);
.getDataAccessTypes()
.add(typeDefinition);
}
}

View File

@@ -2,6 +2,7 @@ package org.hswebframework.web.authorization.basic.embed;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.MapUtils;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.AuthenticationRequest;
import org.hswebframework.web.authorization.builder.DataAccessConfigBuilderFactory;
@@ -69,7 +70,10 @@ public class EmbedAuthenticationProperties implements InitializingBean {
for (Map.Entry<String, Object> stringObjectEntry : objectMap.entrySet()) {
if (stringObjectEntry.getValue() instanceof Map) {
Map<?, ?> mapVal = ((Map) stringObjectEntry.getValue());
boolean maybeIsList = mapVal.keySet().stream().allMatch(org.hswebframework.utils.StringUtils::isInt);
boolean maybeIsList = mapVal
.keySet()
.stream()
.allMatch(org.hswebframework.utils.StringUtils::isInt);
if (maybeIsList) {
stringObjectEntry.setValue(mapVal.values());
}
@@ -82,20 +86,23 @@ public class EmbedAuthenticationProperties implements InitializingBean {
}
public Authentication authenticate(AuthenticationRequest request) {
if(request instanceof PlainTextUsernamePasswordAuthenticationRequest){
if (MapUtils.isEmpty(users)) {
return null;
}
if (request instanceof PlainTextUsernamePasswordAuthenticationRequest) {
PlainTextUsernamePasswordAuthenticationRequest pwdReq = ((PlainTextUsernamePasswordAuthenticationRequest) request);
return users.values()
.stream()
.filter(user ->
pwdReq.getUsername().equals(user.getUsername())
&& pwdReq.getPassword().equals(user.getPassword()))
.findFirst()
.map(EmbedAuthenticationInfo::getId)
.map(authentications::get)
.orElseThrow(() -> new ValidationException("用户不存在"));
for (EmbedAuthenticationInfo user : users.values()) {
if (pwdReq.getUsername().equals(user.getUsername())) {
if (pwdReq.getPassword().equals(user.getPassword())) {
return user.toAuthentication(dataAccessConfigBuilderFactory);
}
return null;
}
}
return null;
}
throw new UnsupportedOperationException("不支持的授权请求:"+request);
throw new UnsupportedOperationException("不支持的授权请求:" + request);
}
public Optional<Authentication> getAuthentication(String userId) {

View File

@@ -1,6 +1,8 @@
package org.hswebframework.web.authorization.basic.embed;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.AuthenticationRequest;
import org.hswebframework.web.authorization.ReactiveAuthenticationManager;
@@ -22,7 +24,16 @@ public class EmbedReactiveAuthenticationManager implements ReactiveAuthenticatio
@Override
public Mono<Authentication> authenticate(Mono<AuthenticationRequest> request) {
return request.map(properties::authenticate);
if (MapUtils.isEmpty(properties.getUsers())) {
return Mono.empty();
}
return request.
handle((req, sink) -> {
Authentication auth = properties.authenticate(req);
if (auth != null) {
sink.next(auth);
}
});
}

View File

@@ -0,0 +1,22 @@
package org.hswebframework.web.authorization.basic.web;
import org.hswebframework.web.authorization.token.ParsedToken;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class BearerTokenParser implements ReactiveUserTokenParser {
@Override
public Mono<ParsedToken> parseToken(ServerWebExchange exchange) {
String token = exchange
.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (token != null && token.startsWith("Bearer ")) {
return Mono.just(ParsedToken.of("bearer", token.substring(7)));
}
return Mono.empty();
}
}

View File

@@ -62,16 +62,6 @@ public class DefaultUserTokenGenPar implements ReactiveUserTokenGenerator, React
if (token == null) {
return Mono.empty();
}
return Mono.just(new ParsedToken() {
@Override
public String getToken() {
return token;
}
@Override
public String getType() {
return getTokenType();
}
});
return Mono.just(ParsedToken.of(getTokenType(),token));
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -40,4 +40,21 @@ public interface AccessTokenManager {
*/
Mono<AccessToken> refreshAccessToken(String clientId, String refreshToken);
/**
* 移除token
*
* @param clientId clientId
* @param token token
* @return void
*/
Mono<Void> removeToken(String clientId, String token);
/**
* 取消对用户的授权
*
* @param clientId clientId
* @param userId 用户ID
* @return void
*/
Mono<Void> cancelGrant(String clientId, String userId);
}

View File

@@ -3,6 +3,8 @@ package org.hswebframework.web.oauth2.server;
import org.hswebframework.web.authorization.ReactiveAuthenticationHolder;
import org.hswebframework.web.authorization.ReactiveAuthenticationManager;
import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser;
import org.hswebframework.web.authorization.token.UserToken;
import org.hswebframework.web.authorization.token.UserTokenManager;
import org.hswebframework.web.oauth2.server.auth.ReactiveOAuth2AccessTokenParser;
import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter;
import org.hswebframework.web.oauth2.server.code.DefaultAuthorizationCodeGranter;
@@ -19,9 +21,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2Properties.class)
@@ -32,13 +36,13 @@ public class OAuth2ServerAutoConfiguration {
@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;
}
// @Bean
// @ConditionalOnBean(AccessTokenManager.class)
// public ReactiveOAuth2AccessTokenParser reactiveOAuth2AccessTokenParser(AccessTokenManager accessTokenManager) {
// ReactiveOAuth2AccessTokenParser parser = new ReactiveOAuth2AccessTokenParser(accessTokenManager);
// ReactiveAuthenticationHolder.addSupplier(parser);
// return parser;
// }
}
@Configuration(proxyBeanMethods = false)
@@ -48,9 +52,11 @@ public class OAuth2ServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AccessTokenManager accessTokenManager(ReactiveRedisConnectionFactory redisConnectionFactory,
public AccessTokenManager accessTokenManager(ReactiveRedisOperations<Object, Object> redis,
UserTokenManager tokenManager,
OAuth2Properties properties) {
RedisAccessTokenManager manager = new RedisAccessTokenManager(redisConnectionFactory);
@SuppressWarnings("all")
RedisAccessTokenManager manager = new RedisAccessTokenManager((ReactiveRedisOperations) redis, tokenManager);
manager.setTokenExpireIn((int) properties.getTokenExpireIn().getSeconds());
manager.setRefreshExpireIn((int) properties.getRefreshTokenIn().getSeconds());
return manager;
@@ -59,15 +65,17 @@ public class OAuth2ServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ClientCredentialGranter clientCredentialGranter(ReactiveAuthenticationManager authenticationManager,
AccessTokenManager accessTokenManager) {
return new DefaultClientCredentialGranter(authenticationManager, accessTokenManager);
AccessTokenManager accessTokenManager,
ApplicationEventPublisher eventPublisher) {
return new DefaultClientCredentialGranter(authenticationManager, accessTokenManager,eventPublisher);
}
@Bean
@ConditionalOnMissingBean
public AuthorizationCodeGranter authorizationCodeGranter(AccessTokenManager tokenManager,
ApplicationEventPublisher eventPublisher,
ReactiveRedisConnectionFactory redisConnectionFactory) {
return new DefaultAuthorizationCodeGranter(tokenManager, redisConnectionFactory);
return new DefaultAuthorizationCodeGranter(tokenManager,eventPublisher, redisConnectionFactory);
}
@Bean

View File

@@ -4,13 +4,16 @@ 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.GrantType;
import org.hswebframework.web.oauth2.OAuth2Constants;
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.event.OAuth2GrantedEvent;
import org.hswebframework.web.oauth2.server.utils.OAuth2ScopeUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
@@ -25,11 +28,15 @@ public class DefaultAuthorizationCodeGranter implements AuthorizationCodeGranter
private final AccessTokenManager accessTokenManager;
private final ApplicationEventPublisher eventPublisher;
private final ReactiveRedisOperations<String, AuthorizationCodeCache> redis;
@SuppressWarnings("all")
public DefaultAuthorizationCodeGranter(AccessTokenManager accessTokenManager, ReactiveRedisConnectionFactory connectionFactory) {
this(accessTokenManager, new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext
public DefaultAuthorizationCodeGranter(AccessTokenManager accessTokenManager,
ApplicationEventPublisher eventPublisher,
ReactiveRedisConnectionFactory connectionFactory) {
this(accessTokenManager, eventPublisher, new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext
.newSerializationContext()
.key((RedisSerializer) RedisSerializer.string())
.value(RedisSerializer.java())
@@ -48,9 +55,12 @@ public class DefaultAuthorizationCodeGranter implements AuthorizationCodeGranter
request.getParameter(OAuth2Constants.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));
codeCache.setAuthentication(authentication.copy(
(permission, action) -> permissionPredicate.test(permission.getId(), action),
dimension -> permissionPredicate.test(dimension.getType().getId(), dimension.getId())));
return redis
@@ -72,16 +82,27 @@ public class DefaultAuthorizationCodeGranter implements AuthorizationCodeGranter
.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))
//移除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);
});
return accessTokenManager
.createAccessToken(cache.getClientId(), cache.getAuthentication(), false)
.flatMap(token -> new OAuth2GrantedEvent(request.getClient(),
token,
cache.getAuthentication(),
cache.getScope(),
GrantType.authorization_code,
request.getParameters())
.publish(eventPublisher)
.onErrorResume(err -> accessTokenManager
.removeToken(cache.getClientId(), token.getAccessToken())
.then(Mono.error(err)))
.thenReturn(token));
})
;
}
}

View File

@@ -2,9 +2,12 @@ package org.hswebframework.web.oauth2.server.credential;
import lombok.AllArgsConstructor;
import org.hswebframework.web.authorization.ReactiveAuthenticationManager;
import org.hswebframework.web.oauth2.GrantType;
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.event.OAuth2GrantedEvent;
import org.springframework.context.ApplicationEventPublisher;
import reactor.core.publisher.Mono;
@AllArgsConstructor
@@ -14,6 +17,8 @@ public class DefaultClientCredentialGranter implements ClientCredentialGranter {
private final AccessTokenManager accessTokenManager;
private final ApplicationEventPublisher eventPublisher;
@Override
public Mono<AccessToken> requestToken(ClientCredentialRequest request) {
@@ -21,6 +26,19 @@ public class DefaultClientCredentialGranter implements ClientCredentialGranter {
return authenticationManager
.getByUserId(client.getUserId())
.flatMap(auth -> accessTokenManager.createAccessToken(client.getClientId(), auth, true));
.flatMap(auth -> accessTokenManager
.createAccessToken(client.getClientId(), auth, true)
.flatMap(token -> new OAuth2GrantedEvent(client,
token,
auth,
"*",
GrantType.client_credentials,
request.getParameters())
.publish(eventPublisher)
.onErrorResume(err -> accessTokenManager
.removeToken(client.getClientId(), token.getAccessToken())
.then(Mono.error(err)))
.thenReturn(token))
);
}
}

View File

@@ -0,0 +1,32 @@
package org.hswebframework.web.oauth2.server.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.event.DefaultAsyncEvent;
import org.hswebframework.web.oauth2.server.AccessToken;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import java.util.Map;
/**
* OAuth2授权成功事件
*
* @author zhouhao
* @since 4.0.15
*/
@Getter
@AllArgsConstructor
public class OAuth2GrantedEvent extends DefaultAsyncEvent {
private final OAuth2Client client;
private final AccessToken accessToken;
private final Authentication authentication;
private final String scope;
private final String grantType;
private final Map<String, String> parameters;
}

View File

@@ -4,6 +4,9 @@ import lombok.Getter;
import lombok.Setter;
import org.apache.commons.codec.digest.DigestUtils;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.token.AuthenticationUserToken;
import org.hswebframework.web.authorization.token.UserTokenManager;
import org.hswebframework.web.authorization.token.redis.RedisUserTokenManager;
import org.hswebframework.web.oauth2.ErrorType;
import org.hswebframework.web.oauth2.OAuth2Exception;
import org.hswebframework.web.oauth2.server.AccessToken;
@@ -13,15 +16,20 @@ 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.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class RedisAccessTokenManager implements AccessTokenManager {
private final ReactiveRedisOperations<String, RedisAccessToken> tokenRedis;
private final UserTokenManager userTokenManager;
@Getter
@Setter
private int tokenExpireIn = 7200;//2小时
@@ -30,37 +38,49 @@ public class RedisAccessTokenManager implements AccessTokenManager {
@Setter
private int refreshExpireIn = 2592000; //30天
public RedisAccessTokenManager(ReactiveRedisOperations<String, RedisAccessToken> tokenRedis) {
public RedisAccessTokenManager(ReactiveRedisOperations<String, RedisAccessToken> tokenRedis,
UserTokenManager userTokenManager) {
this.tokenRedis = tokenRedis;
this.userTokenManager = userTokenManager;
}
@SuppressWarnings("all")
public RedisAccessTokenManager(ReactiveRedisConnectionFactory connectionFactory) {
this(new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext
ReactiveRedisTemplate redis = new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext
.newSerializationContext()
.key((RedisSerializer) RedisSerializer.string())
.value(RedisSerializer.java())
.hashKey(RedisSerializer.string())
.hashValue(RedisSerializer.java())
.build()
));
.build());
this.tokenRedis = redis;
this.userTokenManager = new RedisUserTokenManager(redis);
}
@Override
public Mono<Authentication> getAuthenticationByToken(String accessToken) {
return tokenRedis
.opsForValue()
.get(createTokenRedisKey(accessToken))
.map(RedisAccessToken::getAuthentication);
return userTokenManager
.getByToken(accessToken)
.filter(token -> token instanceof AuthenticationUserToken)
.map(t -> ((AuthenticationUserToken) t).getAuthentication());
}
private String createTokenRedisKey(String token) {
return "oauth2-token:" + token;
private String createTokenRedisKey(String clientId, String token) {
return "oauth2-token:" + clientId + ":" + token;
}
private String createRefreshTokenRedisKey(String token) {
return "oauth2-refresh-token:" + token;
private String createUserTokenRedisKey(RedisAccessToken token) {
return createUserTokenRedisKey(token.getClientId(), token.getAuthentication().getUser().getId());
}
private String createUserTokenRedisKey(String clientId, String userId) {
return "oauth2-user-tokens:" + clientId + ":" + userId;
}
private String createRefreshTokenRedisKey(String clientId, String token) {
return "oauth2-refresh-token:" + clientId + ":" + token;
}
private String createSingletonTokenRedisKey(String clientId) {
@@ -75,12 +95,41 @@ public class RedisAccessTokenManager implements AccessTokenManager {
return storeToken(accessToken).thenReturn(accessToken);
}
private Mono<Void> storeAuthToken(RedisAccessToken token) {
if (token.isSingleton()) {
return userTokenManager
.signIn(token.getAccessToken(),
createTokenType(token.getClientId()),
token.getAuthentication().getUser().getId(),
tokenExpireIn * 1000L)
.then();
} else {
return userTokenManager
.signIn(token.getAccessToken(),
createTokenType(token.getClientId()),
token.getAuthentication().getUser().getId(),
tokenExpireIn * 1000L,
token.getAuthentication())
.then();
}
}
private Mono<Void> 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();
return Flux
.merge(storeAuthToken(token),
tokenRedis
.opsForValue()
.set(createUserTokenRedisKey(token), token, Duration.ofSeconds(tokenExpireIn)),
tokenRedis
.opsForValue()
.set(createTokenRedisKey(token.getClientId(),
token.getAccessToken()), token, Duration.ofSeconds(tokenExpireIn)),
tokenRedis
.opsForValue()
.set(createRefreshTokenRedisKey(token.getClientId(),
token.getRefreshToken()), token, Duration.ofSeconds(refreshExpireIn)))
.then();
}
private Mono<AccessToken> doCreateSingletonAccessToken(String clientId, Authentication authentication) {
@@ -111,7 +160,7 @@ public class RedisAccessTokenManager implements AccessTokenManager {
@Override
public Mono<AccessToken> refreshAccessToken(String clientId, String refreshToken) {
String redisKey = createRefreshTokenRedisKey(refreshToken);
String redisKey = createRefreshTokenRedisKey(clientId, refreshToken);
return tokenRedis
.opsForValue()
@@ -129,10 +178,15 @@ public class RedisAccessTokenManager implements AccessTokenManager {
.as(result -> {
// 单例token
if (token.isSingleton()) {
return tokenRedis
.opsForValue()
.set(createSingletonTokenRedisKey(clientId), token, Duration.ofSeconds(tokenExpireIn))
.then(result);
return userTokenManager
.signOutByToken(token.getAccessToken())
.then(
tokenRedis
.opsForValue()
.set(createSingletonTokenRedisKey(clientId), token, Duration.ofSeconds(tokenExpireIn))
.then(result)
)
;
}
return result;
})
@@ -140,4 +194,58 @@ public class RedisAccessTokenManager implements AccessTokenManager {
});
}
@Override
public Mono<Void> removeToken(String clientId, String token) {
return Flux
.merge(userTokenManager.signOutByToken(token),
tokenRedis.delete(createSingletonTokenRedisKey(clientId)),
tokenRedis.delete(createTokenRedisKey(clientId, token)))
.then();
}
@Override
public Mono<Void> cancelGrant(String clientId, String userId) {
//删除最新的refresh_token
Mono<Void> removeRefreshToken = tokenRedis
.opsForValue()
.get(createUserTokenRedisKey(clientId, userId))
.flatMap(t -> tokenRedis
.opsForValue()
.delete(createRefreshTokenRedisKey(t.getClientId(), t.getRefreshToken())))
.then();
//删除access_token
Mono<Void> removeAccessToken = userTokenManager
.getByUserId(userId)
.flatMap(token -> {
//其他类型的token 忽略
if (!(createTokenType(clientId)).equals(token.getType())) {
return Mono.empty();
}
return tokenRedis
.opsForValue()
.get(createTokenRedisKey(clientId, token.getToken()))
.flatMap(t -> {
//移除token
return tokenRedis
.delete(createTokenRedisKey(t.getClientId(), t.getAccessToken()))
//移除token对应的refresh_token
.then(tokenRedis
.opsForValue()
.delete(createRefreshTokenRedisKey(t.getClientId(), t.getRefreshToken())));
})
.then(userTokenManager.signOutByToken(token.getToken()));
})
.then();
return Flux
.merge(removeRefreshToken, removeAccessToken)
.then();
}
private String createTokenType(String clientId) {
return "oauth2-" + clientId;
}
}

View File

@@ -6,6 +6,10 @@ import org.springframework.util.StringUtils;
import java.util.*;
/**
* <pre>{@code
* role:* user:* device-manager:*
* }</pre>
*
* @author zhouhao
* @since 4.0.8
*/
@@ -23,10 +27,13 @@ public class OAuth2ScopeUtils {
Set<String> acts = actions.computeIfAbsent(per, k -> new HashSet<>());
acts.addAll(Arrays.asList(permissions).subList(1, permissions.length));
}
//全部授权
if (actions.containsKey("*")) {
return ((permission, action) -> true);
}
return ((permission, action) -> Optional
.ofNullable(actions.get(permission))
.map(acts -> action.length == 0 || acts.containsAll(Arrays.asList(action)))
.map(acts -> action.length == 0 || acts.contains("*") || acts.containsAll(Arrays.asList(action)))
.orElse(false));
}
}

View File

@@ -19,7 +19,9 @@ 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.hswebframework.web.oauth2.server.credential.ClientCredentialRequest;
import org.hswebframework.web.oauth2.server.event.OAuth2GrantedEvent;
import org.hswebframework.web.oauth2.server.refresh.RefreshTokenRequest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

View File

@@ -7,6 +7,7 @@ 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 org.springframework.context.support.StaticApplicationContext;
import reactor.test.StepVerifier;
import java.util.Collections;
@@ -20,7 +21,7 @@ public class DefaultAuthorizationCodeGranterTest {
public void testRequestToken() {
DefaultAuthorizationCodeGranter codeGranter = new DefaultAuthorizationCodeGranter(
new RedisAccessTokenManager(RedisHelper.factory), RedisHelper.factory
new RedisAccessTokenManager(RedisHelper.factory), new StaticApplicationContext(), RedisHelper.factory
);
OAuth2Client client = new OAuth2Client();

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-commons</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-commons</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -38,6 +38,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
@@ -112,22 +114,24 @@ public class EasyormConfiguration {
};
}
@Bean
public EntityEventListener entityEventListener(ApplicationEventPublisher eventPublisher,
ObjectProvider<EntityEventListenerCustomizer> customizers) {
DefaultEntityEventListenerConfigure configure = new DefaultEntityEventListenerConfigure();
customizers.forEach(customizer -> customizer.customize(configure));
return new EntityEventListener(eventPublisher, configure);
public CreatorEventListener creatorEventListener() {
return new CreatorEventListener();
}
@Bean
public ValidateEventListener validateEventListener() {
return new ValidateEventListener();
}
@Bean
public CreatorEventListener creatorEventListener() {
return new CreatorEventListener();
public EntityEventListener entityEventListener(ApplicationEventPublisher eventPublisher,
ObjectProvider<EntityEventListenerCustomizer> customizers) {
DefaultEntityEventListenerConfigure configure = new DefaultEntityEventListenerConfigure();
customizers.forEach(customizer -> customizer.customize(configure));
return new EntityEventListener(eventPublisher, configure);
}
@Bean

View File

@@ -27,8 +27,10 @@ import org.hswebframework.web.bean.FastBeanCopier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
@@ -37,9 +39,11 @@ import java.util.function.Supplier;
*/
@SuppressWarnings("unchecked")
public class MapperEntityFactory implements EntityFactory, BeanFactory {
private Map<Class, Mapper> realTypeMapper = new HashMap<>();
private Logger logger = LoggerFactory.getLogger(this.getClass());
private Map<String, PropertyCopier> copierCache = new HashMap<>();
@SuppressWarnings("all")
private final Map<Class<?>, Mapper> realTypeMapper = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@SuppressWarnings("all")
private final Map<String, PropertyCopier> copierCache = new ConcurrentHashMap<>();
private static final DefaultMapperFactory DEFAULT_MAPPER_FACTORY = clazz -> {
String simpleClassName = clazz.getPackage().getName().concat(".Simple").concat(clazz.getSimpleName());
@@ -68,6 +72,16 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
this.realTypeMapper.putAll(realTypeMapper);
}
public <T> MapperEntityFactory addMapping(Class<T> target, Supplier<? extends T> mapper) {
realTypeMapper.put(target, new Mapper(target, mapper));
return this;
}
public <T> MapperEntityFactory addMappingIfAbsent(Class<T> target, Supplier<? extends T> mapper) {
realTypeMapper.putIfAbsent(target, new Mapper(target, mapper));
return this;
}
public <T> MapperEntityFactory addMapping(Class<T> target, Mapper<? extends T> mapper) {
realTypeMapper.put(target, mapper);
return this;
@@ -216,8 +230,8 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
}
public static class Mapper<T> {
Class<T> target;
Supplier<T> instanceGetter;
final Class<T> target;
final Supplier<T> instanceGetter;
public Mapper(Class<T> target, Supplier<T> instanceGetter) {
this.target = target;
@@ -242,16 +256,17 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
}
static class DefaultInstanceGetter<T> implements Supplier<T> {
Class<T> type;
final Constructor<T> constructor;
@SneakyThrows
public DefaultInstanceGetter(Class<T> type) {
this.type = type;
this.constructor = type.getConstructor();
}
@Override
@SneakyThrows
public T get() {
return type.newInstance();
return constructor.newInstance();
}
}
}

View File

@@ -5,7 +5,9 @@ import lombok.Setter;
import org.hswebframework.ezorm.rdb.events.EventContext;
import org.hswebframework.ezorm.rdb.events.EventListener;
import org.hswebframework.ezorm.rdb.events.EventType;
import org.springframework.core.Ordered;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -24,5 +26,6 @@ public class CompositeEventListener implements EventListener {
public void addListener(EventListener eventListener) {
eventListeners.add(eventListener);
eventListeners.sort(Comparator.comparingLong(e -> e instanceof Ordered ? ((Ordered) e).getOrder() : Ordered.LOWEST_PRECEDENCE));
}
}

View File

@@ -12,19 +12,21 @@ import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.validator.CreateGroup;
import org.hswebframework.web.validator.UpdateGroup;
import org.springframework.core.Ordered;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
/**
* 自动填充创建人和修改人信息
*/
public class CreatorEventListener implements EventListener {
public class CreatorEventListener implements EventListener, Ordered {
@Override
public String getId() {
@@ -60,27 +62,44 @@ public class CreatorEventListener implements EventListener {
}
protected void doApplyCreator(EventType type, EventContext context, Authentication auth) {
context.get(MappingContextKeys.instance)
.ifPresent(obj -> {
if (obj instanceof Collection) {
applyCreator(auth, ((Collection<?>) obj), type != MappingEventTypes.update_before);
} else {
applyCreator(auth, obj, type != MappingEventTypes.update_before);
}
});
Object instance = context.get(MappingContextKeys.instance).orElse(null);
if (instance != null) {
if (instance instanceof Collection) {
applyCreator(auth, context, ((Collection<?>) instance), type != MappingEventTypes.update_before);
} else {
applyCreator(auth, context, instance, type != MappingEventTypes.update_before);
}
}
context
.get(MappingContextKeys.updateColumnInstance)
.ifPresent(map -> applyCreator(auth, context, map, type != MappingEventTypes.update_before));
}
public void applyCreator(Authentication auth, Object entity, boolean updateCreator) {
if (updateCreator && entity instanceof RecordCreationEntity) {
RecordCreationEntity e = (RecordCreationEntity) entity;
if (ObjectUtils.isEmpty(e.getCreatorId())) {
e.setCreatorId(auth.getUser().getId());
e.setCreatorName(auth.getUser().getName());
}
if (e.getCreateTime() == null) {
e.setCreateTimeNow();
public void applyCreator(Authentication auth,
EventContext context,
Object entity,
boolean updateCreator) {
long now = System.currentTimeMillis();
if (updateCreator) {
if (entity instanceof RecordCreationEntity) {
RecordCreationEntity e = (RecordCreationEntity) entity;
if (ObjectUtils.isEmpty(e.getCreatorId())) {
e.setCreatorId(auth.getUser().getId());
e.setCreatorName(auth.getUser().getName());
}
if (e.getCreateTime() == null) {
e.setCreateTime(now);
}
} else if (entity instanceof Map) {
Map<Object, Object> map = ((Map) entity);
map.putIfAbsent("creatorId", auth.getUser().getId());
map.putIfAbsent("creatorName", auth.getUser().getName());
map.putIfAbsent("createTime", now);
}
}
if (entity instanceof RecordModifierEntity) {
RecordModifierEntity e = (RecordModifierEntity) entity;
@@ -89,15 +108,26 @@ public class CreatorEventListener implements EventListener {
e.setModifierName(auth.getUser().getName());
}
if (e.getModifyTime() == null) {
e.setModifyTimeNow();
e.setModifyTime(now);
}
} else if (entity instanceof Map) {
Map<Object, Object> map = ((Map) entity);
map.putIfAbsent("modifierId", auth.getUser().getId());
map.putIfAbsent("modifierName", auth.getUser().getName());
map.putIfAbsent("modifyTime", now);
}
}
public void applyCreator(Authentication auth, Collection<?> entities, boolean updateCreator) {
public void applyCreator(Authentication auth, EventContext context, Collection<?> entities, boolean updateCreator) {
for (Object entity : entities) {
applyCreator(auth, entity, updateCreator);
applyCreator(auth, context, entity, updateCreator);
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -22,6 +22,7 @@ import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.event.AsyncEvent;
import org.hswebframework.web.event.GenericsPayloadApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.Ordered;
import reactor.core.publisher.Mono;
import reactor.function.Function3;
import reactor.util.function.Tuple2;
@@ -36,7 +37,10 @@ import static org.hswebframework.web.crud.events.EntityEventHelper.*;
@SuppressWarnings("all")
@AllArgsConstructor
public class EntityEventListener implements EventListener {
public class EntityEventListener implements EventListener, Ordered {
public static final ContextKey<List<Object>> readyToDeleteContextKey = ContextKey.of("readyToDelete");
public static final ContextKey<List<Object>> readyToUpdateContextKey = ContextKey.of("readyToUpdate");
private final ApplicationEventPublisher eventPublisher;
@@ -133,39 +137,37 @@ public class EntityEventListener implements EventListener {
protected List<Object> createAfterData(List<Object> olds,
EventContext context) {
List<Object> newValues = new ArrayList<>(olds.size());
EntityColumnMapping mapping = context
.get(MappingContextKeys.columnMapping)
.orElseThrow(UnsupportedOperationException::new);
TableOrViewMetadata table = context.get(ContextKeys.table).orElseThrow(UnsupportedOperationException::new);
RDBColumnMetadata idColumn = table
.getColumns()
.stream()
.filter(RDBColumnMetadata::isPrimaryKey)
.findFirst()
.orElse(null);
if (idColumn == null) {
return Collections.emptyList();
}
Map<String, Object> columns = context
.get(MappingContextKeys.updateColumnInstance)
.orElse(Collections.emptyMap());
for (Object old : olds) {
Object newValue = context
.get(MappingContextKeys.updateColumnInstance)
.map(map -> {
Object data = FastBeanCopier.copy(map, FastBeanCopier.copy(old, mapping.newInstance()));
for (Map.Entry<String, Object> entry : map.entrySet()) {
//set null
if (entry.getValue() == null
|| entry.getValue() instanceof NullValue) {
GlobalConfig
.getPropertyOperator()
.setProperty(data, entry.getKey(), null);
}
}
return data;
})
.orElseThrow(() -> {
return new IllegalArgumentException("can not get update instance");
});
newValues.add(newValue);
Object data = FastBeanCopier.copy(old, mapping.newInstance());
for (Map.Entry<String, Object> entry : columns.entrySet()) {
RDBColumnMetadata column = mapping.getColumnByName(entry.getKey()).orElse(null);
if (column == null) {
continue;
}
Object value = entry.getValue();
//set null
if (value instanceof NullValue) {
value = null;
}
GlobalConfig
.getPropertyOperator()
.setProperty(data, column.getAlias(), value);
}
newValues.add(data);
}
return newValues;
}
@@ -230,6 +232,7 @@ public class EntityEventListener implements EventListener {
holder.invoke(this.doAsyncEvent(() -> {
Tuple2<List<Object>, List<Object>> _tmp = updated.get();
if (_tmp != null) {
return sendUpdateEvent(_tmp.getT1(),
_tmp.getT2(),
entityType,
@@ -297,6 +300,9 @@ public class EntityEventListener implements EventListener {
.setParam(dslUpdate.toQueryParam())
.fetch()
.collectList()
.doOnNext(list->{
context.set(readyToDeleteContextKey, list);
})
.filter(CollectionUtils::isNotEmpty)
.flatMap(list -> {
deleted.set(list);
@@ -468,4 +474,9 @@ public class EntityEventListener implements EventListener {
return eventSupplier.get();
});
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
}

View File

@@ -10,11 +10,12 @@ import org.hswebframework.web.api.crud.entity.Entity;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.validator.CreateGroup;
import org.hswebframework.web.validator.UpdateGroup;
import org.springframework.core.Ordered;
import java.util.List;
import java.util.Optional;
public class ValidateEventListener implements EventListener {
public class ValidateEventListener implements EventListener, Ordered {
@Override
public String getId() {
@@ -35,9 +36,10 @@ public class ValidateEventListener implements EventListener {
resultHolder
.ifPresent(holder -> holder
.invoke(LocaleUtils
.currentReactive()
.doOnNext(locale -> LocaleUtils.doWith(locale, (l) -> tryValidate(type, context)))
.then()
.doInReactive(() -> {
tryValidate(type, context);
return null;
})
));
} else {
tryValidate(type, context);
@@ -72,4 +74,9 @@ public class ValidateEventListener implements EventListener {
.ifPresent(entity -> entity.tryValidate(UpdateGroup.class));
}
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1000;
}
}

View File

@@ -1,5 +1,6 @@
package org.hswebframework.web.crud.web;
import io.r2dbc.spi.R2dbcException;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.CodeConstants;
import org.hswebframework.web.authorization.exception.AccessDenyException;
@@ -15,6 +16,7 @@ import org.hswebframework.web.logger.ReactiveLogger;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.TransactionException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
@@ -43,6 +45,25 @@ import java.util.stream.Collectors;
@Order
public class CommonErrorControllerAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<ResponseMessage<Object>> handleException(TransactionException e) {
log.error(e.getMessage(), e);
return LocaleUtils
.resolveMessageReactive("error.internal_server_error")
.map(msg -> ResponseMessage.error(500, "error." + e.getClass().getSimpleName(), msg));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<ResponseMessage<Object>> handleException(R2dbcException e) {
log.error(e.getMessage(), e);
return LocaleUtils
.resolveMessageReactive("error.internal_server_error")
.map(msg -> ResponseMessage.error(500, "error." + e.getClass().getSimpleName(), msg));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<ResponseMessage<Object>> handleException(BusinessException e) {

View File

@@ -1,6 +1,5 @@
package org.hswebframework.web.crud.web;
import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
import org.hswebframework.web.i18n.WebFluxLocaleFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -33,12 +32,6 @@ public class CommonWebFluxConfiguration {
return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry);
}
@Bean
public R2dbcDataIntegrityViolationException r2dbcDataIntegrityViolationException(){
return new R2dbcDataIntegrityViolationException();
}
@Bean
public WebFilter localeWebFilter() {
return new WebFluxLocaleFilter();

View File

@@ -20,6 +20,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
@@ -82,17 +83,21 @@ public class ResponseMessageWrapperAdvice implements ResponseBodyAdvice<Object>
if (body instanceof Mono) {
return ((Mono<?>) body)
.map(ResponseMessage::ok)
.switchIfEmpty(Mono.just(ResponseMessage.ok()));
.switchIfEmpty(Mono.fromSupplier(ResponseMessage::ok));
}
if (body instanceof Flux) {
return ((Flux<?>) body)
.collectList()
.map(ResponseMessage::ok)
.switchIfEmpty(Mono.just(ResponseMessage.ok()));
.switchIfEmpty(Mono.fromSupplier(ResponseMessage::ok));
}
if (body instanceof String) {
Method method = returnType.getMethod();
if (method != null && returnType.getMethod().getReturnType() == String.class) {
return JSON.toJSONString(ResponseMessage.ok(body));
}
return ResponseMessage.ok(body);
}

View File

@@ -2,4 +2,5 @@ error.unsupported_media_type=Unsupported media type
error.not_acceptable_media_type=Not acceptable media type
error.method_not_allowed=Method not allowed
error.duplicate_data=Duplicate data
error.data_error=Data error
error.data_error=Data error
error.internal_server_error = Internal server error

View File

@@ -2,4 +2,5 @@ error.unsupported_media_type=不支持的请求类型
error.not_acceptable_media_type=不支持的媒体类型
error.method_not_allowed=不支持的请求方法
error.duplicate_data=重复的数据
error.data_error=数据错误
error.data_error=数据错误
error.internal_server_error=服务器内部错误

View File

@@ -23,7 +23,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-concurrent</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -64,9 +64,8 @@ public class RedisReactiveCache<E> implements ReactiveCache<E> {
}
protected <T> Mono<T> handleError(Throwable error) {
return Mono.fromRunnable(() -> {
log.error(error.getMessage(), error);
});
log.error(error.getMessage(), error);
return Mono.empty();
}
@Override

View File

@@ -15,10 +15,11 @@ import java.util.function.Supplier;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UnSupportedReactiveCache<E> implements ReactiveCache<E> {
private static final UnSupportedReactiveCache INSTANCE = new UnSupportedReactiveCache();
private static final UnSupportedReactiveCache<?> INSTANCE = new UnSupportedReactiveCache<>();
@SuppressWarnings("all")
public static <E> ReactiveCache<E> getInstance() {
return INSTANCE;
return (UnSupportedReactiveCache)INSTANCE;
}
@Override

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -118,7 +118,18 @@
<dependency>
<artifactId>jctools-core</artifactId>
<groupId>org.jctools</groupId>
<version>2.1.2</version>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
</dependency>
<dependency>
<groupId>io.seruco.encoding</groupId>
<artifactId>base62</artifactId>
<version>0.1.3</version>
</dependency>
</dependencies>

View File

@@ -21,6 +21,7 @@ import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -513,6 +514,8 @@ public final class FastBeanCopier {
if (targetClass == List.class) {
return new ArrayList<>();
} else if (targetClass == ConcurrentHashMap.KeySetView.class) {
return ConcurrentHashMap.newKeySet();
} else if (targetClass == Set.class) {
return new HashSet<>();
} else if (targetClass == Queue.class) {
@@ -568,13 +571,15 @@ public final class FastBeanCopier {
return (T) new Date(((Date) source).getTime());
}
}
if (Collection.class.isAssignableFrom(targetClass)) {
if (target.isCollectionType()) {
Collection collection = newCollection(targetClass);
Collection sourceCollection;
if (source instanceof Collection) {
sourceCollection = (Collection) source;
} else if (source instanceof Object[]) {
sourceCollection = Arrays.asList((Object[]) source);
} else if (source instanceof Map) {
sourceCollection = ((Map<?, ?>) source).values();
} else {
if (source instanceof String) {
String stringValue = ((String) source);
@@ -619,7 +624,7 @@ public final class FastBeanCopier {
Enum t = ((Enum<?>) e);
if ((t.name().equalsIgnoreCase(strSource)
|| Objects.equals(String.valueOf(t.ordinal()), strSource))) {
return (T)e;
return (T) e;
}
}
@@ -642,7 +647,20 @@ public final class FastBeanCopier {
//快速复制map
if (targetClass == Map.class) {
if (source instanceof Map) {
return (T) new HashMap(((Map<?, ?>) source));
return (T) copyMap(((Map<?, ?>) source));
}
if (source instanceof Collection) {
Map<Object, Object> map = new LinkedHashMap<>();
int i = 0;
for (Object o : ((Collection<?>) source)) {
if (genericType.length >= 2) {
map.put(convert(i++, genericType[0], EMPTY_CLASS_ARRAY), convert(o, genericType[1], EMPTY_CLASS_ARRAY));
} else {
map.put(i++, o);
}
}
return (T) map;
}
ClassDescription sourType = ClassDescriptions.getDescription(source.getClass());
return (T) copy(source, Maps.newHashMapWithExpectedSize(sourType.getFieldSize()));
@@ -656,6 +674,22 @@ public final class FastBeanCopier {
// return null;
}
private Map<?, ?> copyMap(Map<?, ?> map) {
if (map instanceof TreeMap) {
return new TreeMap<>(map);
}
if (map instanceof LinkedHashMap) {
return new LinkedHashMap<>(map);
}
if (map instanceof ConcurrentHashMap) {
return new ConcurrentHashMap<>(map);
}
return new HashMap<>(map);
}
private Object converterByApache(Class<?> targetClass, Object source) {
org.apache.commons.beanutils.Converter converter = convertUtils.lookup(targetClass);
if (null != converter) {

View File

@@ -17,6 +17,7 @@ public class ContextUtils {
return contextThreadLocal.get();
}
@Deprecated
public static Mono<Context> reactiveContext() {
return Mono
.subscriberContext()
@@ -32,6 +33,7 @@ public class ContextUtils {
}));
}
@Deprecated
public static Function<reactor.util.context.Context, reactor.util.context.Context> acceptContext(Consumer<Context> contextConsumer) {
return context -> {
if (!context.hasKey(Context.class)) {

View File

@@ -8,10 +8,10 @@ import java.util.function.Function;
public class DefaultAsyncEvent implements AsyncEvent {
private Mono<?> async = Mono.empty();
private Mono<?> first = Mono.empty();
private transient Mono<?> async = Mono.empty();
private transient Mono<?> first = Mono.empty();
private boolean hasListener;
private transient boolean hasListener;
public synchronized void async(Publisher<?> publisher) {
hasListener = true;

View File

@@ -5,6 +5,7 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.i18n.LocaleUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.util.Locale;
@@ -19,7 +20,7 @@ import java.util.Objects;
*/
@Getter
@Setter(AccessLevel.PROTECTED)
public class I18nSupportException extends RuntimeException {
public class I18nSupportException extends TraceSourceException {
/**
* 消息code,在message.properties文件中定义的key
@@ -70,4 +71,29 @@ public class I18nSupportException extends RuntimeException {
.currentReactive()
.map(this::getLocalizedMessage);
}
public static String tryGetLocalizedMessage(Throwable error, Locale locale) {
if (error instanceof I18nSupportException) {
return ((I18nSupportException) error).getLocalizedMessage(locale);
}
String msg = error.getMessage();
if (!StringUtils.hasText(msg)) {
msg = "error." + error.getClass().getSimpleName();
}
if (msg.contains(".")) {
return LocaleUtils.resolveMessage(msg, locale, msg);
}
return msg;
}
public static String tryGetLocalizedMessage(Throwable error) {
return tryGetLocalizedMessage(error, LocaleUtils.current());
}
public static Mono<String> tryGetLocalizedMessageReactive(Throwable error) {
return LocaleUtils
.currentReactive()
.map(locale -> tryGetLocalizedMessage(error, locale));
}
}

View File

@@ -0,0 +1,159 @@
package org.hswebframework.web.exception;
import org.hswebframework.web.i18n.LocaleUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Function;
/**
* 支持溯源的异常,通过{@link TraceSourceException#withSource(Object) }来标识异常的源头.
* 在捕获异常的地方通过获取异常源来处理一些逻辑,比如判断是由哪条数据发生的错误等操作.
*
* @author zhouhao
* @since 4.0.15
*/
public class TraceSourceException extends RuntimeException {
private static final String deepTraceKey = TraceSourceException.class.getName() + "_deep";
private static final Context deepTraceContext = Context.of(deepTraceKey, true);
private String operation;
private Object source;
public TraceSourceException() {
}
public TraceSourceException(String message) {
super(message);
}
public TraceSourceException(Throwable e) {
super(e.getMessage(),e);
}
public TraceSourceException(String message, Throwable e) {
super(message, e);
}
@Nullable
public Object getSource() {
return source;
}
@Nullable
public String getOperation() {
return operation;
}
public TraceSourceException withSource(Object source) {
this.source = source;
return self();
}
public TraceSourceException withSource(String operation, Object source) {
this.operation = operation;
this.source = source;
return self();
}
protected TraceSourceException self() {
return this;
}
/**
* 深度溯源上下文,用来标识是否是深度溯源的异常.开启深度追踪后,会创建新的{@link TraceSourceException}对象.
*
* @return 上下文
* @see reactor.core.publisher.Flux#subscriberContext(Context)
* @see Mono#subscriberContext(Context)
*/
public static Context deepTraceContext() {
return deepTraceContext;
}
public static <T> Function<Throwable, Mono<T>> transfer(Object source) {
return transfer(null, source);
}
/**
* 溯源异常转换器.通常配合{@link Mono#onErrorResume(Function)}使用.
* <p>
* 转换逻辑:
* <p>
* 1. 如果捕获的异常不是TraceSourceException,则直接创建新的TraceSourceException并返回.
* <p>
* 2. 如果捕获的异常是TraceSourceException,并且上下文没有指定{@link TraceSourceException#deepTraceContext()},
* 则修改捕获的TraceSourceException异常中的source.如果上下文中指定了{@link TraceSourceException#deepTraceContext()}
* 则创建新的TraceSourceException
*
* <pre>{@code
*
* doSomething()
* .onErrorResume(TraceSourceException.transfer(data))
*
* }</pre>
*
* @param operation 操作名称
* @param source 源
* @param <T> 泛型
* @return 转换器
* @see reactor.core.publisher.Flux#onErrorResume(Function)
* @see Mono#onErrorResume(Function)
*/
public static <T> Function<Throwable, Mono<T>> transfer(String operation, Object source) {
if (source == null && operation == null) {
return Mono::error;
}
return err -> {
if (err instanceof TraceSourceException) {
return Mono
.deferWithContext(ctx -> {
if (ctx.hasKey(deepTraceKey)) {
return Mono.error(new TraceSourceException(err).withSource(operation,source));
} else {
return Mono.error(((TraceSourceException) err).withSource(operation,source));
}
});
}
return Mono.error(new TraceSourceException(err).withSource(operation,source));
};
}
public static Object tryGetSource(Throwable err) {
if (err instanceof TraceSourceException) {
return ((TraceSourceException) err).getSource();
}
return null;
}
public static String tryGetOperation(Throwable err) {
if (err instanceof TraceSourceException) {
return ((TraceSourceException) err).getOperation();
}
return null;
}
public static String tryGetOperationLocalized(Throwable err, Locale locale) {
String opt = tryGetOperation(err);
return StringUtils.hasText(opt) ? LocaleUtils.resolveMessage(opt, locale, opt) : opt;
}
public static Mono<String> tryGetOperationLocalizedReactive(Throwable err) {
return LocaleUtils
.currentReactive()
.handle((locale, sink) -> {
String opt = tryGetOperationLocalized(err, locale);
if (opt != null) {
sink.next(opt);
}
});
}
}

View File

@@ -1,15 +1,18 @@
package org.hswebframework.web.i18n;
import io.netty.util.concurrent.FastThreadLocal;
import lombok.AllArgsConstructor;
import org.hswebframework.web.exception.I18nSupportException;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import org.springframework.context.MessageSource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Signal;
import reactor.core.publisher.SignalType;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.*;
import reactor.util.context.Context;
import javax.annotation.Nonnull;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@@ -23,7 +26,6 @@ import java.util.function.Function;
* <li>{@link LocaleUtils#current()} </li>
* <li>{@link LocaleUtils#currentReactive()}</li>
* <li>{@link LocaleUtils#resolveMessageReactive(String, Object...)}</li>
* <li>{@link LocaleUtils#doOnNext(BiConsumer)}</li>
* </ul>
*
* @author zhouhao
@@ -33,7 +35,7 @@ public final class LocaleUtils {
public static final Locale DEFAULT_LOCALE = Locale.getDefault();
private static final ThreadLocal<Locale> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
private static final FastThreadLocal<Locale> CONTEXT_THREAD_LOCAL = new FastThreadLocal<>();
static MessageSource messageSource = UnsupportedMessageSource.instance();
@@ -63,11 +65,12 @@ public final class LocaleUtils {
* @return 返回值
*/
public static <T, R> R doWith(T data, Locale locale, BiFunction<T, Locale, R> mapper) {
Locale old = CONTEXT_THREAD_LOCAL.get();
try {
CONTEXT_THREAD_LOCAL.set(locale);
return mapper.apply(data, locale);
} finally {
CONTEXT_THREAD_LOCAL.remove();
CONTEXT_THREAD_LOCAL.set(old);
}
}
@@ -78,11 +81,12 @@ public final class LocaleUtils {
* @param consumer 任务
*/
public static void doWith(Locale locale, Consumer<Locale> consumer) {
Locale old = CONTEXT_THREAD_LOCAL.get();
try {
CONTEXT_THREAD_LOCAL.set(locale);
consumer.accept(locale);
} finally {
CONTEXT_THREAD_LOCAL.remove();
CONTEXT_THREAD_LOCAL.set(old);
}
}
@@ -112,6 +116,23 @@ public final class LocaleUtils {
.subscriberContext()
.map(ctx -> ctx.getOrDefault(Locale.class, DEFAULT_LOCALE));
}
public static <T> Mono<T> doInReactive(Callable<T> call) {
return currentReactive()
.handle((locale, sink) -> {
Locale old = CONTEXT_THREAD_LOCAL.get();
try {
CONTEXT_THREAD_LOCAL.set(locale);
T data = call.call();
if (data != null) {
sink.next(data);
}
} catch (Throwable e) {
sink.error(e);
} finally {
CONTEXT_THREAD_LOCAL.set(old);
}
});
}
/**
* 响应式方式解析出异常的区域消息,并进行结果转换.
@@ -450,4 +471,92 @@ public final class LocaleUtils {
return doOn(SignalType.ON_ERROR, (s, l) -> operation.accept(s.getThrowable(), l));
}
public static <T> Flux<T> transform(Flux<T> flux) {
return new LocaleFlux<>(flux);
}
public static <T> Mono<T> transform(Mono<T> mono) {
return new LocaleMono<>(mono);
}
@AllArgsConstructor
static class LocaleMono<T> extends Mono<T> {
private final Mono<T> source;
@Override
public void subscribe(@Nonnull CoreSubscriber<? super T> actual) {
doWith(actual,
actual.currentContext().getOrDefault(Locale.class, DEFAULT_LOCALE),
(a, l) -> {
source.subscribe(
new LocaleSwitchSubscriber<>(a)
);
return null;
}
);
}
}
@AllArgsConstructor
static class LocaleFlux<T> extends Flux<T> {
private final Flux<T> source;
@Override
public void subscribe(@Nonnull CoreSubscriber<? super T> actual) {
doWith(actual,
actual.currentContext().getOrDefault(Locale.class, DEFAULT_LOCALE),
(a, l) -> {
source.subscribe(
new LocaleSwitchSubscriber<>(a)
);
return null;
}
);
}
}
@AllArgsConstructor
static class LocaleSwitchSubscriber<T> extends BaseSubscriber<T> {
private final CoreSubscriber<T> actual;
@Override
@Nonnull
public Context currentContext() {
return actual
.currentContext();
}
@Override
protected void hookOnSubscribe(@Nonnull Subscription subscription) {
actual.onSubscribe(this);
}
private Locale current() {
return currentContext()
.getOrDefault(Locale.class, DEFAULT_LOCALE);
}
@Override
protected void hookOnComplete() {
doWith(current(), (l) -> actual.onComplete());
}
@Override
protected void hookOnError(@Nonnull Throwable error) {
doWith(error, current(), (v, l) -> {
actual.onError(v);
return null;
});
}
@Override
protected void hookOnNext(@Nonnull T value) {
doWith(value, current(), (v, l) -> {
actual.onNext(v);
return null;
});
}
}
}

View File

@@ -15,6 +15,7 @@ public class WebFluxLocaleFilter implements WebFilter {
public Mono<Void> filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) {
return chain
.filter(exchange)
.as(LocaleUtils::transform)
.subscriberContext(LocaleUtils.useLocale(getLocaleContext(exchange)));
}

View File

@@ -18,13 +18,8 @@
package org.hswebframework.web.id;
import org.hswebframework.utils.RandomUtil;
import org.hswebframework.web.utils.DigestUtils;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* ID生成器,用于生成ID
*
@@ -53,7 +48,7 @@ public interface IDGenerator<T> {
/**
* 随机字符
*/
IDGenerator<String> RANDOM = RandomUtil::randomChar;
IDGenerator<String> RANDOM = RandomIdGenerator.GLOBAL;
/**
* md5(uuid())

View File

@@ -0,0 +1,64 @@
package org.hswebframework.web.id;
import io.netty.util.concurrent.FastThreadLocal;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class RandomIdGenerator implements IDGenerator<String> {
// java -Dgenerator.random.instance-id=8
static final RandomIdGenerator GLOBAL = new RandomIdGenerator(
Integer.getInteger("generator.random.instance-id", ThreadLocalRandom.current().nextInt(1,127)).byteValue()
);
static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
private final static FastThreadLocal<byte[]> HOLDER = new FastThreadLocal<byte[]>() {
@Override
protected byte[] initialValue() {
return new byte[24];
}
};
private final byte instanceId;
public static RandomIdGenerator create(byte instanceId) {
return new RandomIdGenerator(instanceId);
}
public String generate() {
long now = System.currentTimeMillis();
byte[] value = HOLDER.get();
value[0] = instanceId;
value[1] = (byte) (now >>> 32);
value[2] = (byte) (now >>> 24);
value[3] = (byte) (now >>> 16);
value[4] = (byte) (now >>> 8);
value[5] = (byte) (now);
nextBytes(value, 6, 8);
nextBytes(value, 8, 16);
nextBytes(value, 16, 24);
return encoder.encodeToString(value);
}
private static void nextBytes(byte[] bytes, int offset, int len) {
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int i = offset; i < len; ) {
for (int rnd = random.nextInt(),
n = Math.min(len - i, Integer.SIZE / Byte.SIZE);
n-- > 0; rnd >>= Byte.SIZE) {
bytes[i++] = (byte) rnd;
}
}
}
}

View File

@@ -5,26 +5,27 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
@Slf4j
public class SnowflakeIdGenerator {
private long workerId;
private long dataCenterId;
private final long workerId;
private final long dataCenterId;
private long sequence = 0L;
private long twepoch = 1288834974657L;
private final long twepoch = 1288834974657L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDataCenterId = -1L ^ (-1L << datacenterIdBits);
private long sequenceBits = 12L;
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long maxDataCenterId = ~(-1L << datacenterIdBits);
private final long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long sequenceMask = ~(-1L << sequenceBits);
private long lastTimestamp = -1L;
@@ -41,7 +42,15 @@ public class SnowflakeIdGenerator {
return generator;
}
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
public static SnowflakeIdGenerator create(int workerId, int dataCenterId) {
return new SnowflakeIdGenerator(workerId, dataCenterId);
}
public static SnowflakeIdGenerator create() {
return create(ThreadLocalRandom.current().nextInt(31), ThreadLocalRandom.current().nextInt(31));
}
private SnowflakeIdGenerator(long workerId, long dataCenterId) {
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));

View File

@@ -1,6 +1,9 @@
package org.hswebframework.web.utils;
import io.netty.util.concurrent.FastThreadLocal;
import io.seruco.encoding.base62.Base62;
import org.apache.commons.codec.binary.Hex;
import org.hswebframework.web.id.RandomIdGenerator;
import java.security.MessageDigest;
import java.util.function.Consumer;
@@ -8,10 +11,33 @@ import java.util.function.Supplier;
public class DigestUtils {
public static final ThreadLocal<MessageDigest> md5 = ThreadLocal.withInitial(org.apache.commons.codec.digest.DigestUtils::getMd5Digest);
public static final ThreadLocal<MessageDigest> sha256 = ThreadLocal.withInitial(org.apache.commons.codec.digest.DigestUtils::getSha256Digest);
public static final ThreadLocal<MessageDigest> sha1 = ThreadLocal.withInitial(org.apache.commons.codec.digest.DigestUtils::getSha1Digest);
public static final FastThreadLocal<MessageDigest> md5 = new FastThreadLocal<MessageDigest>() {
@Override
protected MessageDigest initialValue() {
return org.apache.commons.codec.digest.DigestUtils.getMd5Digest();
}
};
public static final FastThreadLocal<MessageDigest> sha256 = new FastThreadLocal<MessageDigest>() {
@Override
protected MessageDigest initialValue() {
return org.apache.commons.codec.digest.DigestUtils.getSha256Digest();
}
};
public static final FastThreadLocal<MessageDigest> sha1 = new FastThreadLocal<MessageDigest>() {
@Override
protected MessageDigest initialValue() {
return org.apache.commons.codec.digest.DigestUtils.getSha1Digest();
}
};
private static final Base62 base62 = Base62.createInstance();
public static Base62 base62(){
return base62;
}
public static byte[] md5(Consumer<MessageDigest> digestHandler) {
return digest(md5::get, digestHandler);
}
@@ -47,6 +73,9 @@ public class DigestUtils {
public static String md5Hex(String str) {
return Hex.encodeHexString(md5(str.getBytes()));
}
public static String md5Base62(String str) {
return new String(base62.encode(md5(str.getBytes())));
}
public static byte[] sha256(byte[] data) {
return org.apache.commons.codec.digest.DigestUtils.digest(sha256.get(), data);

View File

@@ -38,7 +38,7 @@ public final class ValidatorUtils {
public static <T> T tryValidate(T bean, Class<?>... group) {
Set<ConstraintViolation<T>> violations = getValidator().validate(bean, group);
if (!violations.isEmpty()) {
throw new ValidationException(violations);
throw new ValidationException(violations).withSource(bean);
}
return bean;
@@ -47,7 +47,7 @@ public final class ValidatorUtils {
public static <T> T tryValidate(T bean, String property, Class<?>... group) {
Set<ConstraintViolation<T>> violations = getValidator().validateProperty(bean, property, group);
if (!violations.isEmpty()) {
throw new ValidationException(violations);
throw new ValidationException(violations).withSource(bean);
}
return bean;
@@ -56,7 +56,7 @@ public final class ValidatorUtils {
public static <T> void tryValidate(Class<T> bean, String property, Object value, Class<?>... group) {
Set<ConstraintViolation<T>> violations = getValidator().validateValue(bean, property, value, group);
if (!violations.isEmpty()) {
throw new ValidationException(violations);
throw new ValidationException(violations).withSource(value);
}
}

View File

@@ -1,16 +1,14 @@
package org.hswebframework.web.bean;
import com.google.common.collect.ImmutableMap;
import jdk.nashorn.internal.objects.annotations.Getter;
import lombok.Getter;
import lombok.Setter;
import org.junit.Assert;
import org.junit.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -61,6 +59,43 @@ public class FastBeanCopierTest {
}
@Test
public void testMapList() {
Map<String, Object> data = new HashMap<>();
data.put("templates", new HashMap() {
{
put("0", Collections.singletonMap("name", "test"));
put("1", Collections.singletonMap("name", "test"));
}
});
Config config = FastBeanCopier.copy(data, new Config());
Assert.assertNotNull(config);
Assert.assertNotNull(config.templates);
System.out.println(config.templates);
Assert.assertEquals(2,config.templates.size());
}
@Getter
@Setter
public static class Config {
private List<Template> templates;
}
@Getter
@Setter
public static class Template {
private String name;
@Override
public String toString() {
return "name:"+name;
}
}
@Test
public void testCopyMap() {
@@ -87,10 +122,10 @@ public class FastBeanCopierTest {
@Test
public void testProxy() {
AtomicReference<Object> reference=new AtomicReference<>();
AtomicReference<Object> reference = new AtomicReference<>();
ProxyTest test = (ProxyTest) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{ProxyTest.class}, (proxy, method, args) -> {
new Class[]{ProxyTest.class}, (proxy, method, args) -> {
if (method.getName().equals("getName")) {
return "test";
}
@@ -105,20 +140,20 @@ public class FastBeanCopierTest {
Target source = new Target();
FastBeanCopier.copy(test,source);
Assert.assertEquals(source.getName(),test.getName());
FastBeanCopier.copy(test, source);
Assert.assertEquals(source.getName(), test.getName());
source.setName("test2");
FastBeanCopier.copy(source,test);
FastBeanCopier.copy(source, test);
Assert.assertEquals(reference.get(),source.getName());
Assert.assertEquals(reference.get(), source.getName());
}
@Test
public void testGetProperty(){
public void testGetProperty() {
Assert.assertEquals(1,FastBeanCopier.getProperty(ImmutableMap.of("a",1,"b",2),"a"));
Assert.assertEquals(1, FastBeanCopier.getProperty(ImmutableMap.of("a", 1, "b", 2), "a"));
}

View File

@@ -2,6 +2,8 @@ package org.hswebframework.web.i18n;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.Locale;
@@ -11,15 +13,35 @@ public class LocaleUtilsTest {
@Test
public void testOnNext() {
public void testFlux() {
Flux.just(1)
.as(LocaleUtils.doOnNext((i, l) -> {
.as(LocaleUtils::transform)
.doOnNext(i -> {
assertEquals(i.intValue(), 1);
assertEquals(l, Locale.CHINA);
}))
.subscriberContext(LocaleUtils.useLocale(Locale.CHINA))
assertEquals(LocaleUtils.current(), Locale.ENGLISH);
})
.subscriberContext(LocaleUtils.useLocale(Locale.ENGLISH))
.blockLast();
}
@Test
public void testMono() {
Mono.just(1)
.doOnNext(i -> {
assertEquals(i.intValue(), 1);
assertEquals(LocaleUtils.current(), Locale.ENGLISH);
})
.as(LocaleUtils::transform)
.subscriberContext(LocaleUtils.useLocale(Locale.ENGLISH))
.block();
LocaleUtils
.doInReactive(()->{
assertEquals(LocaleUtils.current(), Locale.ENGLISH);
return null;
})
.subscriberContext(LocaleUtils.useLocale(Locale.ENGLISH))
.block();
}
}

View File

@@ -3,6 +3,9 @@ package org.hswebframework.web.id;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
/**
* @author zhouhao
* @since 3.0
@@ -19,9 +22,19 @@ public class IDGeneratorTests {
Assert.assertNotNull(IDGenerator.RANDOM.generate());
Assert.assertNotNull(IDGenerator.SNOW_FLAKE.generate());
Assert.assertNotNull(IDGenerator.SNOW_FLAKE_HEX.generate());
for (int i = 0; i < 100; i++) {
System.out.println(IDGenerator.RANDOM.generate());
}
for (int i = 0; i < 100; i++) {
System.out.println(IDGenerator.SNOW_FLAKE.generate());
}
long time = System.currentTimeMillis();
Set<String> set =new HashSet<>(100_0000);
for (int i = 0; i < 100_0000; i++) {
set.add(IDGenerator.RANDOM.generate());
}
System.out.println(set.size());
System.out.println((System.currentTimeMillis()-time));
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-datasource</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-datasource</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-datasource</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-logging</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -39,7 +39,7 @@ import java.util.concurrent.atomic.AtomicReference;
* @author zhouhao
* @since 3.0
*/
public class ReactiveAopAccessLoggerSupport extends StaticMethodMatcherPointcutAdvisor implements WebFilter {
public class ReactiveAopAccessLoggerSupport extends StaticMethodMatcherPointcutAdvisor implements WebFilter{
@Autowired(required = false)
private final List<AccessLoggerParser> loggerParsers = new ArrayList<>();
@@ -58,9 +58,11 @@ public class ReactiveAopAccessLoggerSupport extends StaticMethodMatcherPointcutA
AccessLoggerInfo info = createLogger(methodInterceptorHolder);
Object response = methodInvocation.proceed();
if (response instanceof Mono) {
return wrapMonoResponse(((Mono<?>) response), info);
return wrapMonoResponse(((Mono<?>) response), info)
.subscriberContext(Context.of(AccessLoggerInfo.class, info));
} else if (response instanceof Flux) {
return wrapFluxResponse(((Flux<?>) response), info);
return wrapFluxResponse(((Flux<?>) response), info)
.subscriberContext(Context.of(AccessLoggerInfo.class, info));
}
return response;
});
@@ -183,7 +185,7 @@ public class ReactiveAopAccessLoggerSupport extends StaticMethodMatcherPointcutA
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
@@ -217,10 +219,12 @@ public class ReactiveAopAccessLoggerSupport extends StaticMethodMatcherPointcutA
return info;
}
@AllArgsConstructor
@EqualsAndHashCode
private static class CacheKey{
private static class CacheKey {
private Class<?> type;
private Method method;
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-logging</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -23,7 +23,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -28,12 +28,10 @@ import org.springframework.util.MimeType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -70,23 +68,19 @@ public class CustomJackson2JsonDecoder extends Jackson2CodecSupport implements H
ObjectReader reader = getObjectReader(elementType, hints);
return LocaleUtils
.currentReactive()
.flatMapMany(locale -> tokens
.handle((tokenBuffer, sink) -> {
LocaleUtils.doWith(locale, l -> {
try {
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
logValue(value, hints);
if (value != null) {
sink.next(value);
}
} catch (IOException ex) {
sink.error(processException(ex));
}
});
}));
return tokens
.as(LocaleUtils::transform)
.handle((tokenBuffer, sink) -> {
try {
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
logValue(value, hints);
if (value != null) {
sink.next(value);
}
} catch (IOException ex) {
sink.error(processException(ex));
}
});
}
@Override
@@ -94,15 +88,10 @@ public class CustomJackson2JsonDecoder extends Jackson2CodecSupport implements H
public Mono<Object> decodeToMono(@NonNull Publisher<DataBuffer> input, @NonNull ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return LocaleUtils
.currentReactive()
.flatMap(locale -> DataBufferUtils
.join(input)
.map(dataBuffer -> LocaleUtils
.doWith(dataBuffer,
locale,
(buf, l) -> decode(buf, elementType, mimeType, hints)))
);
return DataBufferUtils
.join(input)
.as(LocaleUtils::transform)
.map(dataBuffer -> decode(dataBuffer, elementType, mimeType, hints));
}
@Override

View File

@@ -116,72 +116,55 @@ public class CustomJackson2jsonEncoder extends Jackson2CodecSupport implements H
Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
return LocaleUtils
.currentReactive()
.flatMapMany(locale -> {
if (inputStream instanceof Mono) {
return Mono.from(inputStream)
.map(value -> LocaleUtils
.doWith(value, locale,
((val, loc) ->
encodeValue(val, bufferFactory, elementType, mimeType, hints)
)
))
.flux();
} else {
byte[] separator = streamSeparator(mimeType);
if (separator != null) { // streaming
try {
ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer
.getFactory()
._getBufferRecycler());
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = getObjectMapper()
.getFactory()
.createGenerator(byteBuilder, encoding);
SequenceWriter sequenceWriter = writer.writeValues(generator);
if (inputStream instanceof Mono) {
return Mono.from(inputStream)
.as(LocaleUtils::transform)
.map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints))
.flux();
} else {
byte[] separator = streamSeparator(mimeType);
if (separator != null) { // streaming
try {
ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer
.getFactory()
._getBufferRecycler());
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = getObjectMapper()
.getFactory()
.createGenerator(byteBuilder, encoding);
SequenceWriter sequenceWriter = writer.writeValues(generator);
return Flux
.from(inputStream)
.map(value -> LocaleUtils
.doWith(value,
locale,
((val, loc) -> this
.encodeStreamingValue(val,
bufferFactory,
hints,
sequenceWriter,
byteBuilder,
separator)
)
))
.doAfterTerminate(() -> {
try {
byteBuilder.release();
generator.close();
} catch (IOException ex) {
logger.error("Could not close Encoder resources", ex);
}
});
} catch (IOException ex) {
return Flux.error(ex);
}
} else { // non-streaming
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
return Flux.from(inputStream)
.collectList()
.map(value -> LocaleUtils
.doWith(value, locale,
((val, loc) ->
encodeValue(val, bufferFactory, listType, mimeType, hints)
)
))
.flux();
}
return Flux
.from(inputStream)
.as(LocaleUtils::transform)
.map(value -> this.encodeStreamingValue(value,
bufferFactory,
hints,
sequenceWriter,
byteBuilder,
separator))
.doAfterTerminate(() -> {
try {
byteBuilder.release();
generator.close();
} catch (IOException ex) {
logger.error("Could not close Encoder resources", ex);
}
});
} catch (IOException ex) {
return Flux.error(ex);
}
} else { // non-streaming
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
return Flux.from(inputStream)
.collectList()
.as(LocaleUtils::transform)
.map(value -> encodeValue(value, bufferFactory, listType, mimeType, hints))
.flux();
}
}
});
}
}
@Override

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-system-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -1,5 +1,7 @@
package org.hswebframework.web.system.authorization.api.entity;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -9,15 +11,18 @@ import org.apache.commons.codec.digest.DigestUtils;
import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
import org.hswebframework.web.api.crud.entity.GenericEntity;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
import org.hswebframework.web.bean.ToString;
import org.hswebframework.web.validator.CreateGroup;
import org.springframework.util.StringUtils;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import java.util.Objects;
/**
* 系统用户实体
@@ -85,7 +90,10 @@ public class UserEntity extends GenericEntity<String> implements RecordCreationE
return super.getId();
}
public void generateId(){
setId(DigestUtils.md5Hex(username));
public void generateId() {
if (StringUtils.hasText(getId())) {
return;
}
setId(DigestUtils.md5Hex(username));
}
}

View File

@@ -16,6 +16,8 @@ import org.hswebframework.web.system.authorization.api.entity.UserEntity;
@AllArgsConstructor
@Getter
public class UserModifiedEvent extends DefaultAsyncEvent {
private UserEntity before;
private UserEntity userEntity;
private boolean passwordModified;

View File

@@ -29,6 +29,8 @@ public interface ReactiveUserService {
*/
Mono<Boolean> saveUser(Mono<UserEntity> userEntity);
Mono<UserEntity> addUser(UserEntity userEntity);
/**
* 根据用户名查询用户实体,如果用户不存在则返回{@link Mono#empty()}
*

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-system-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -55,10 +55,10 @@ public class AuthorizationServiceAutoConfiguration {
return new DefaultDimensionService();
}
@Bean
public UserDimensionProvider userPermissionDimensionProvider() {
return new UserDimensionProvider();
}
// @Bean
// public UserDimensionProvider userPermissionDimensionProvider() {
// return new UserDimensionProvider();
// }
@Bean
public DefaultDimensionUserService defaultDimensionUserService() {

View File

@@ -56,9 +56,6 @@ public class DefaultDimensionUserService extends GenericReactiveCrudService<Dime
public void dispatchDimensionBind(EntitySavedEvent<DimensionUserEntity> event) {
event.async(
this.publishEvent(Flux.fromIterable(event.getEntity()), DimensionBindEvent::new)
.then(
this.clearUserCache(event.getEntity())
)
);
}
@@ -67,9 +64,6 @@ public class DefaultDimensionUserService extends GenericReactiveCrudService<Dime
public void dispatchDimensionBind(EntityCreatedEvent<DimensionUserEntity> event) {
event.async(
this.publishEvent(Flux.fromIterable(event.getEntity()), DimensionBindEvent::new)
.then(
this.clearUserCache(event.getEntity())
)
);
}
@@ -78,9 +72,6 @@ public class DefaultDimensionUserService extends GenericReactiveCrudService<Dime
public void dispatchDimensionUnbind(EntityDeletedEvent<DimensionUserEntity> event) {
event.async(
this.publishEvent(Flux.fromIterable(event.getEntity()), DimensionUnbindEvent::new)
.then(
this.clearUserCache(event.getEntity())
)
);
}

View File

@@ -10,6 +10,7 @@ import org.hswebframework.web.authorization.ReactiveAuthenticationInitializeServ
import org.hswebframework.web.authorization.access.DataAccessConfig;
import org.hswebframework.web.authorization.access.DataAccessType;
import org.hswebframework.web.authorization.builder.DataAccessConfigBuilderFactory;
import org.hswebframework.web.authorization.events.AuthorizationInitializeEvent;
import org.hswebframework.web.authorization.simple.SimpleAuthentication;
import org.hswebframework.web.authorization.simple.SimplePermission;
import org.hswebframework.web.authorization.simple.SimpleUser;
@@ -21,6 +22,7 @@ import org.hswebframework.web.system.authorization.api.entity.PermissionEntity;
import org.hswebframework.web.system.authorization.api.entity.UserEntity;
import org.hswebframework.web.system.authorization.api.service.reactive.ReactiveUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -48,6 +50,9 @@ public class DefaultReactiveAuthenticationInitializeService
@Autowired(required = false)
private List<DimensionProvider> dimensionProviders = new ArrayList<>();
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
public Mono<Authentication> initUserAuthorization(String userId) {
return doInit(userService.findById(userId));
@@ -58,54 +63,57 @@ public class DefaultReactiveAuthenticationInitializeService
return userEntityMono.flatMap(user -> {
SimpleAuthentication authentication = new SimpleAuthentication();
authentication.setUser(SimpleUser
.builder()
.id(user.getId())
.name(user.getName())
.username(user.getUsername())
.userType(user.getType())
.build());
.builder()
.id(user.getId())
.name(user.getName())
.username(user.getUsername())
.userType(user.getType())
.build());
return initPermission(authentication)
.switchIfEmpty(Mono.just(authentication))
.defaultIfEmpty(authentication)
.onErrorResume(err -> {
log.warn(err.getMessage(), err);
return Mono.just(authentication);
})
.flatMap(auth -> {
AuthorizationInitializeEvent event = new AuthorizationInitializeEvent(auth);
return event
.publish(eventPublisher)
.then(Mono.fromSupplier(event::getAuthentication));
});
});
}
protected Flux<AuthorizationSettingEntity> getSettings(List<Dimension> dimensions) {
return Flux.fromIterable(dimensions)
.filter(dimension -> dimension.getType() != null)
.groupBy(d -> d.getType().getId(), (Function<Dimension, Object>) Dimension::getId)
.flatMap(group ->
group.collectList()
.flatMapMany(list -> settingRepository
.createQuery()
.where(AuthorizationSettingEntity::getState, 1)
.and(AuthorizationSettingEntity::getDimensionType, group.key())
.in(AuthorizationSettingEntity::getDimensionTarget, list)
.fetch()));
.filter(dimension -> dimension.getType() != null)
.groupBy(d -> d.getType().getId(), (Function<Dimension, Object>) Dimension::getId)
.flatMap(group ->
group.collectList()
.flatMapMany(list -> settingRepository
.createQuery()
.where(AuthorizationSettingEntity::getState, 1)
.and(AuthorizationSettingEntity::getDimensionType, group.key())
.in(AuthorizationSettingEntity::getDimensionTarget, list)
.fetch()));
}
protected Mono<Authentication> initPermission(SimpleAuthentication authentication) {
return Flux.fromIterable(dimensionProviders)
.flatMap(provider -> provider.getDimensionByUserId(authentication.getUser().getId()))
.cast(Dimension.class)
.collectList()
.doOnNext(authentication::setDimensions)
.flatMap(allDimension ->
Mono.zip(
getAllPermission()
, getSettings(allDimension)
.collect(Collectors.groupingBy(AuthorizationSettingEntity::getPermission))
, (_p, _s) -> handlePermission(authentication, allDimension, _p, _s)
));
.flatMap(provider -> provider.getDimensionByUserId(authentication.getUser().getId()))
.cast(Dimension.class)
.doOnNext(authentication::addDimension)
.collectList()
.then(Mono.defer(() -> Mono
.zip(getAllPermission(),
getSettings(authentication.getDimensions()).collect(Collectors.groupingBy(AuthorizationSettingEntity::getPermission)),
(_p, _s) -> handlePermission(authentication, _p, _s)
)));
}
protected SimpleAuthentication handlePermission(SimpleAuthentication authentication,
List<Dimension> dimensionList,
Map<String, PermissionEntity> permissions,
Map<String, List<AuthorizationSettingEntity>> settings) {
Map<String, PermissionEntity> permissionMap = new HashMap<>();
@@ -134,16 +142,19 @@ public class DefaultReactiveAuthenticationInitializeService
if (permissionSetting.getDataAccesses() != null) {
permissionSetting.getDataAccesses()
.stream()
.map(conf -> {
DataAccessConfig config = builderFactory.create().fromMap(conf.toMap()).build();
if (config == null) {
log.warn("unsupported data access:{}", conf.toMap());
}
return config;
})
.filter(Objects::nonNull)
.forEach(configs::add);
.stream()
.map(conf -> {
DataAccessConfig config = builderFactory
.create()
.fromMap(conf.toMap())
.build();
if (config == null) {
log.warn("unsupported data access:{}", conf.toMap());
}
return config;
})
.filter(Objects::nonNull)
.forEach(configs::add);
}
if (CollectionUtils.isNotEmpty(permissionSetting.getActions())) {
permission.getActions().addAll(permissionSetting.getActions());

View File

@@ -24,6 +24,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.ValidationException;
import java.util.Objects;
public class DefaultReactiveUserService extends GenericReactiveCrudService<UserEntity, String> implements ReactiveUserService {
@@ -60,11 +61,20 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
return doAdd(userEntity);
}
return findById(userEntity.getId())
.flatMap(ignore -> doUpdate(userEntity))
.switchIfEmpty(Mono.error(NotFoundException::new));
.flatMap(old -> doUpdate(old, userEntity))
.switchIfEmpty(
Objects.equals(userEntity.getId(), userEntity.getUsername()) ?
doAdd(userEntity) :
Mono.error(NotFoundException::new)
);
}).thenReturn(true);
}
@Override
public Mono<UserEntity> addUser(UserEntity userEntity) {
return doAdd(userEntity);
}
protected Mono<UserEntity> doAdd(UserEntity userEntity) {
return Mono
@@ -94,22 +104,29 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
}
protected Mono<UserEntity> doUpdate(UserEntity userEntity) {
protected Mono<UserEntity> doUpdate(UserEntity old, UserEntity newer) {
return Mono
.defer(() -> {
boolean passwordChanged = StringUtils.hasText(userEntity.getPassword());
if (passwordChanged) {
userEntity.setSalt(IDGenerator.RANDOM.generate());
passwordValidator.validate(userEntity.getPassword());
userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword(), userEntity.getSalt()));
boolean updatePassword = StringUtils.hasText(newer.getPassword());
boolean passwordChanged = updatePassword &&
!Objects.equals(
passwordEncoder.encode(newer.getPassword(), old.getSalt()),
old.getPassword()
);
if (updatePassword) {
newer.setSalt(IDGenerator.RANDOM.generate());
passwordValidator.validate(newer.getPassword());
newer.setPassword(passwordEncoder.encode(newer.getPassword(), newer.getSalt()));
}
return getRepository()
.createUpdate()
.set(userEntity)
.where(userEntity::getId)
.set(newer)
.where(newer::getId)
.execute()
.flatMap(__ -> new UserModifiedEvent(userEntity, passwordChanged).publish(eventPublisher))
.thenReturn(userEntity)
.flatMap(__ -> new UserModifiedEvent(old, newer, passwordChanged).publish(eventPublisher))
.thenReturn(newer)
.flatMap(e -> ClearUserAuthorizationCacheEvent
.of(e.getId())
.publish(eventPublisher)
@@ -175,12 +192,22 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
return findById(userId)
.switchIfEmpty(Mono.error(NotFoundException::new))
.filter(user -> passwordEncoder.encode(oldPassword, user.getSalt()).equals(user.getPassword()))
.switchIfEmpty(Mono.error(() -> new ValidationException("密码错误")))
.flatMap(user -> repository
.createUpdate()
.set(UserEntity::getPassword, passwordEncoder.encode(newPassword, user.getSalt()))
.where(user::getId)
.execute())
.switchIfEmpty(Mono.error(() -> new ValidationException("error.illegal_user_password")))
.flatMap(old -> {
String encodePwd = passwordEncoder.encode(newPassword, old.getSalt());
boolean passwordChanged = !Objects.equals(encodePwd, old.getPassword());
UserEntity newer = old.copyTo(new UserEntity());
newer.setPassword(encodePwd);
return repository
.createUpdate()
.set(newer::getPassword)
.where(newer::getId)
.execute()
.flatMap(e -> new UserModifiedEvent(old, newer, passwordChanged)
.publish(eventPublisher)
.thenReturn(e));
})
.map(i -> i > 0);
}

View File

@@ -28,19 +28,19 @@ public class DimensionTerm extends AbstractTermFragmentBuilder {
}
public static <T extends Conditional<?>> T inject(T query,
String column,
String dimensionType,
List<String> userId) {
String column,
String dimensionType,
List<String> userId) {
return inject(query, column, dimensionType, false, false, userId);
}
public static <T extends Conditional<?>> T inject(T query,
String column,
String dimensionType,
boolean not,
boolean any,
List<String> userId) {
return (T)query.accept(column, createTermType(dimensionType, not, any), userId);
String column,
String dimensionType,
boolean not,
boolean any,
List<String> userId) {
return (T) query.accept(column, createTermType(dimensionType, not, any), userId);
}
public static String createTermType(String dimensionType, boolean not, boolean any) {
@@ -73,7 +73,7 @@ public class DimensionTerm extends AbstractTermFragmentBuilder {
fragments.addSql("not ");
}
fragments
.addSql("exists(select 1 from s_dimension_user d where d.dimension_type_id = ? and d.dimension_id =", columnFullName)
.addSql("exists(select 1 from", getTableName("s_dimension_user", column), "d where d.dimension_type_id = ? and d.dimension_id =", columnFullName)
.addParameter(options.get(0));
if (!options.contains("any")) {

View File

@@ -37,7 +37,7 @@ public class UserDimensionTerm extends AbstractTermFragmentBuilder {
fragments.addSql("not");
}
fragments.addSql("exists(select 1 from s_dimension_user d where d.user_id =", columnFullName);
fragments.addSql("exists(select 1 from ",getTableName("s_dimension_user",column)," d where d.user_id =", columnFullName);
if (options.size() > 0) {
String typeId = options.get(0);

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-system-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-system</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-system</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-system</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>hsweb-framework</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -24,7 +24,7 @@
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-framework</artifactId>
<version>4.0.15-SNAPSHOT</version>
<version>4.0.15</version>
<modules>
<module>hsweb-starter</module>
<module>hsweb-core</module>
@@ -90,7 +90,7 @@
<cglib.version>3.2.2</cglib.version>
<aspectj.version>1.6.12</aspectj.version>
<hsweb.ezorm.version>4.0.14</hsweb.ezorm.version>
<hsweb.ezorm.version>4.1.0-SNAPSHOT</hsweb.ezorm.version>
<hsweb.utils.version>3.0.2</hsweb.utils.version>
<hsweb.expands.version>3.0.2</hsweb.expands.version>
<swagger.version>2.7.0</swagger.version>