i18n 支持

This commit is contained in:
zhou-hao
2021-06-21 18:21:20 +08:00
parent 208f9b34dd
commit e664d68023
22 changed files with 218 additions and 45 deletions

View File

@@ -55,5 +55,5 @@ public @interface TwoFactor {
* @return 错误提示
* @since 3.0.6
*/
String message() default "需要进行双因子验证";
String message() default "assert.verify_code_error";
}

View File

@@ -1,9 +1,12 @@
package org.hswebframework.web.authorization.exception;
import lombok.Getter;
import org.hswebframework.web.exception.I18nSupportException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.Set;
/**
* 权限验证异常
*
@@ -11,7 +14,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
* @since 3.0
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
public class AccessDenyException extends RuntimeException {
public class AccessDenyException extends I18nSupportException {
private static final long serialVersionUID = -5135300127303801430L;
@@ -19,16 +22,21 @@ public class AccessDenyException extends RuntimeException {
private String code;
public AccessDenyException() {
this("权限不足,拒绝访问!");
this("error.access_denied");
}
public AccessDenyException(String message) {
super(message);
}
public AccessDenyException(String permission, Set<String> actions) {
super("error.permission_denied", permission, actions);
}
public AccessDenyException(String message, String code) {
this(message, code, null);
}
public AccessDenyException(String message, Throwable cause) {
this(message, "access_denied", cause);
}

View File

@@ -1,9 +1,10 @@
package org.hswebframework.web.authorization.exception;
import lombok.Getter;
import org.hswebframework.web.exception.I18nSupportException;
@Getter
public class AuthenticationException extends RuntimeException {
public class AuthenticationException extends I18nSupportException {
public static String ILLEGAL_PASSWORD = "illegal_password";
@@ -13,6 +14,10 @@ public class AuthenticationException extends RuntimeException {
private final String code;
public AuthenticationException(String code) {
this(code, "error." + code);
}
public AuthenticationException(String code, String message) {
super(message);
this.code = code;

View File

@@ -19,6 +19,7 @@
package org.hswebframework.web.authorization.exception;
import org.hswebframework.web.authorization.token.TokenState;
import org.hswebframework.web.exception.I18nSupportException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -29,7 +30,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
* @since 3.0
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class UnAuthorizedException extends RuntimeException {
public class UnAuthorizedException extends I18nSupportException {
private static final long serialVersionUID = 2422918455013900645L;
private final TokenState state;

View File

@@ -253,7 +253,7 @@ public class DefaultUserTokenManager implements UserTokenManager {
.flatMap(this::checkTimeout)
.filterWhen(t -> {
if (t.isNormal()) {
return Mono.error(new AccessDenyException("该用户已在其他地方登陆"));
return Mono.error(new AccessDenyException("error.logged_in_elsewhere"));
}
return Mono.empty();
})

View File

@@ -13,30 +13,30 @@ public enum TokenState implements EnumDict<String> {
/**
* 正常,有效
*/
normal("normal","正常"),
normal("normal","message.token_state_normal"),
/**
* 已被禁止访问
*/
deny("deny", "已被禁止访问"),
deny("deny", "message.token_state_deny"),
/**
* 已过期
*/
expired("expired", "用户未登录"),
expired("expired", "message.token_state_expired"),
/**
* 已被踢下线
* @see AllopatricLoginMode#offlineOther
*/
offline("offline", "用户已在其他地方登录"),
offline("offline", "message.token_state_offline"),
/**
* 锁定
*/
lock("lock", "登录状态已被锁定");
lock("lock", "message.token_state_lock");
private String value;
private final String value;
private String text;
private final String text;
}

View File

@@ -244,7 +244,7 @@ public class RedisUserTokenManager implements UserTokenManager {
return userIsLoggedIn(userId)
.flatMap(r -> {
if (r) {
return Mono.error(new AccessDenyException("已在其他地方登录", TokenState.deny.getValue(), null));
return Mono.error(new AccessDenyException("error.logged_in_elsewhere", TokenState.deny.getValue(), null));
}
return doSign;
});

View File

@@ -0,0 +1,14 @@
error.access_denied=权限不足,拒绝访问!
error.permission_denied=当前用户无权限[{0}]:{1}
error.logged_in_elsewhere=该用户已在其他地方登陆
#
message.token_state_normal=正常
message.token_state_deny=已被禁止访问
message.token_state_expired=用户未登录
message.token_state_offline=用户已在其他地方登录
message.token_state_lock=登录状态已被锁定
#
assert.need_two_factor_verify=需要双因子验证
assert.username_must_not_be_empty=用户名不能为空
assert.password_must_not_be_empty=密码不能为空
assert.verify_code_error=验证码错误

View File

@@ -36,7 +36,7 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
private ResourcesDefinition resources = new ResourcesDefinition();
private DimensionsDefinition dimensions = new DimensionsDefinition();
private String message = "权限不足,拒绝访问";
private String message = "error.access_denied";
private Phased phased;

View File

@@ -22,7 +22,7 @@ import javax.servlet.http.HttpServletResponse;
@AllArgsConstructor
public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapter {
private TwoFactorValidatorManager validatorManager;
private final TwoFactorValidatorManager validatorManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -45,9 +45,9 @@ public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapte
code = request.getHeader(factor.parameter());
}
if (StringUtils.isEmpty(code)) {
throw new NeedTwoFactorException(factor.message(), factor.provider());
throw new NeedTwoFactorException("assert.need_two_factor_verify", factor.provider());
} else if (!validator.verify(code, factor.timeout())) {
throw new NeedTwoFactorException("验证码错误", factor.provider());
throw new NeedTwoFactorException(factor.message(), factor.provider());
}
}
return super.preHandle(request, response, handler);

View File

@@ -85,8 +85,8 @@ public class AuthorizationController {
String username_ = (String) parameters.get("username");
String password_ = (String) parameters.get("password");
Assert.hasLength(username_, "用户名不能为空");
Assert.hasLength(password_, "密码不能为空");
Assert.hasLength(username_, "assert.username_must_not_be_empty");
Assert.hasLength(password_, "assert.password_must_not_be_empty");
Function<String, Object> parameterGetter = parameters::get;
return Mono.defer(() -> {
@@ -101,7 +101,7 @@ public class AuthorizationController {
.publish(eventPublisher)
.then(authenticationManager
.authenticate(Mono.just(new PlainTextUsernamePasswordAuthenticationRequest(username, password)))
.switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD,"密码错误")))
.switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD)))
.flatMap(auth -> {
//触发授权成功事件
AuthorizationSuccessEvent event = new AuthorizationSuccessEvent(auth, parameterGetter);

View File

@@ -1,7 +1,6 @@
package org.hswebframework.web.crud.web;
import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
import io.r2dbc.spi.R2dbcNonTransientException;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.authorization.exception.AccessDenyException;
import org.hswebframework.web.authorization.exception.AuthenticationException;
@@ -10,11 +9,11 @@ import org.hswebframework.web.authorization.token.TokenState;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.exception.NotFoundException;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.logger.ReactiveLogger;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.Ordered;
import org.springframework.context.MessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.core.codec.DecodingException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
@@ -40,11 +39,20 @@ import java.util.stream.Collectors;
@Order
public class CommonErrorControllerAdvice {
private final MessageSource messageSource;
public CommonErrorControllerAdvice(MessageSource messageSource) {
this.messageSource = messageSource;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<ResponseMessage<Object>> handleException(BusinessException e) {
return Mono.just(ResponseMessage.error(e.getCode(), e.getMessage()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
return LocaleUtils
.resolveThrowable(messageSource,
e,
(err, msg) -> ResponseMessage.error(err.getStatus(), err.getCode(), msg));
}
@ExceptionHandler

View File

@@ -1,14 +1,22 @@
package org.hswebframework.web.crud.web;
import org.hswebframework.web.i18n.WebFluxLocaleFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@@ -16,17 +24,31 @@ public class CommonWebFluxConfiguration {
@Bean
@ConditionalOnMissingBean
public CommonErrorControllerAdvice commonErrorControllerAdvice(){
return new CommonErrorControllerAdvice();
public CommonErrorControllerAdvice commonErrorControllerAdvice(MessageSource messageSource) {
return new CommonErrorControllerAdvice(messageSource);
}
@Bean
@ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper",name = "enabled",havingValue = "true",matchIfMissing = true)
@ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper", name = "enabled", havingValue = "true", matchIfMissing = true)
@ConfigurationProperties(prefix = "hsweb.webflux.response-wrapper")
public ResponseMessageWrapper responseMessageWrapper(ServerCodecConfigurer codecConfigurer,
RequestedContentTypeResolver resolver,
ReactiveAdapterRegistry registry){
return new ResponseMessageWrapper(codecConfigurer.getWriters(),resolver,registry);
ReactiveAdapterRegistry registry) {
return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry);
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean
public WebFilter localeWebFilter() {
return new WebFluxLocaleFilter();
}
}

View File

@@ -26,7 +26,7 @@ import lombok.Getter;
* @author zhouhao
* @since 2.0
*/
public class BusinessException extends RuntimeException {
public class BusinessException extends I18nSupportException {
private static final long serialVersionUID = 5441923856899380112L;
@Getter
@@ -39,20 +39,20 @@ public class BusinessException extends RuntimeException {
this(message, 500);
}
public BusinessException(String message, String code) {
this(message, code, 500);
public BusinessException(String message, int status, Object... args) {
this(message, null, status, args);
}
public BusinessException(String message, String code, int status) {
super(message);
public BusinessException(String message, String code, Object... args) {
this(message, code, 500, args);
}
public BusinessException(String message, String code, int status, Object... args) {
super(message, args);
this.code = code;
this.status = status;
}
public BusinessException(String message, int status) {
super(message);
this.status = status;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);

View File

@@ -0,0 +1,27 @@
package org.hswebframework.web.exception;
import lombok.Getter;
@Getter
public class I18nSupportException extends RuntimeException {
private final Object[] args;
public I18nSupportException(String code, Object... args) {
super(code);
this.args = args;
}
public I18nSupportException(String code, Throwable cause, Object... args) {
super(code, cause);
this.args = args;
}
@Override
public String getLocalizedMessage() {
// TODO: 2021/6/21
return super.getLocalizedMessage();
}
}

View File

@@ -24,11 +24,11 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(message, 404);
public NotFoundException(String message, Object... args) {
super(message, 404, args);
}
public NotFoundException() {
this("记录不存在");
this("error.not_found");
}
}

View File

@@ -0,0 +1,67 @@
package org.hswebframework.web.i18n;
import org.hswebframework.web.exception.I18nSupportException;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import reactor.core.publisher.Mono;
import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Function;
public class LocaleUtils {
public static final LocaleContext DEFAULT_CONTEXT = new SimpleLocaleContext(Locale.getDefault());
public static Mono<LocaleContext> reactive() {
return Mono
.subscriberContext()
.map(ctx -> ctx
.<LocaleContext>getOrEmpty(LocaleContext.class)
.orElse(DEFAULT_CONTEXT));
}
public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(MessageSource messageSource,
S source,
BiFunction<S, String, R> mapper) {
return doWithReactive(messageSource, source, Throwable::getMessage, mapper, source.getArgs());
}
public static <S extends Throwable, R> Mono<R> resolveThrowable(MessageSource messageSource,
S source,
BiFunction<S, String, R> mapper,
Object... args) {
return doWithReactive(messageSource, source, Throwable::getMessage, mapper, args);
}
public static <S, R> Mono<R> doWithReactive(MessageSource messageSource,
S source,
Function<S, String> message,
BiFunction<S, String, R> mapper,
Object... args) {
return reactive()
.map(ctx -> {
String msg = message.apply(source);
String newMsg = resolveMessage(messageSource, msg, ctx.getLocale(), msg, args);
return mapper.apply(source, newMsg);
});
}
public static Mono<String> reactiveMessage(MessageSource messageSource,
String code,
Object... args) {
return reactive()
.map(ctx -> resolveMessage(messageSource, code, ctx.getLocale(), code, args));
}
public static String resolveMessage(MessageSource messageSource,
String code,
Locale locale,
String defaultMessage,
Object... args) {
return messageSource.getMessage(code, args, defaultMessage, locale);
}
}

View File

@@ -0,0 +1,18 @@
package org.hswebframework.web.i18n;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.lang.NonNull;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
public class WebFluxLocaleFilter implements WebFilter {
@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) {
return chain
.filter(exchange)
.subscriberContext(ctx -> ctx.put(LocaleContext.class, exchange.getLocaleContext()));
}
}

View File

@@ -0,0 +1,2 @@
error.not_found=数据不存在
error.cant_create_instance=无法创建实例:{0}

View File

@@ -75,7 +75,7 @@ public class PermissionProperties {
.map(Permission::getActions)
.orElseGet(Collections::emptySet));
throw new AccessDenyException("当前用户无权限:" + setting.getPermission() + "" +actions);
throw new AccessDenyException(setting.getPermission(), actions);
}
};

View File

@@ -59,7 +59,7 @@ public class DefaultDimensionUserService extends GenericReactiveCrudService<Dime
return this
.publishEvent(entityPublisher, DimensionBindEvent::new)
.as(super::insert)
.onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("重复的绑定请求"));
.onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("error.duplicate_key"));
}
@Override