mirror of
https://github.com/hs-web/hsweb-framework.git
synced 2026-06-06 05:17:46 +08:00
i18n 支持
This commit is contained in:
@@ -55,5 +55,5 @@ public @interface TwoFactor {
|
||||
* @return 错误提示
|
||||
* @since 3.0.6
|
||||
*/
|
||||
String message() default "需要进行双因子验证";
|
||||
String message() default "assert.verify_code_error";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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=验证码错误
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
error.not_found=数据不存在
|
||||
error.cant_create_instance=无法创建实例:{0}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
error.duplicate_key=重复的请求
|
||||
Reference in New Issue
Block a user