From 768033f221db42a07ecb86a06c3340ea87c4fc6e Mon Sep 17 00:00:00 2001 From: zhouhao Date: Tue, 4 Dec 2018 18:39:04 +0800 Subject: [PATCH] =?UTF-8?q?#103=20=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=8F=8C=E9=87=8D=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/authorization/Authentication.java | 5 - .../authorization/annotation/Authorize.java | 5 - .../authorization/annotation/TwoFactor.java | 4 +- .../exception/AccessDenyException.java | 10 + .../exception/NeedTwoFactorException.java | 19 ++ .../setting/SettingNullValueHolder.java | 56 +++++ .../setting/SettingValueHolder.java | 26 ++ .../setting/StringSourceSettingHolder.java | 98 ++++++++ .../setting/UserSettingManager.java | 13 + .../setting/UserSettingPermission.java | 26 ++ ...DefaultAuthorizationAutoConfiguration.java | 8 + .../twofactor/TwoFactorToken.java | 13 + .../twofactor/TwoFactorTokenManager.java | 9 + .../twofactor/TwoFactorValidator.java | 7 +- .../twofactor/TwoFactorValidatorManager.java | 9 +- .../twofactor/TwoFactorValidatorProvider.java | 12 + .../defaults/DefaultTwoFactorValidator.java | 38 +++ .../DefaultTwoFactorValidatorManager.java | 47 ++++ .../DefaultTwoFactorValidatorProvider.java | 30 +++ .../HashMapTwoFactorTokenManager.java | 70 ++++++ .../UnsupportedTwoFactorValidator.java | 26 ++ .../HashMapTwoFactorTokenManagerTest.java | 29 +++ .../AopAuthorizeAutoConfiguration.java | 1 + .../AuthorizingHandlerAutoConfiguration.java | 24 +- .../TwoFactorHandlerInterceptorAdapter.java | 52 ++++ .../full/FullFunctionTest.groovy | 23 ++ .../full/controller/TestCrudController.java | 8 + .../TestTwoFactorValidatorProvider.java | 39 +++ .../src/test/resources/application.yml | 111 +++++---- .../RestControllerExceptionTranslator.java | 10 + .../authorization/UserSettingEntity.java | 15 +- .../service/authorization/UserService.java | 11 + .../events/TotpTwoFactorCreatedEvent.java | 18 ++ .../events/UserCreatedEvent.java | 16 ++ .../simple/InServiceUserSettingManager.java | 41 +++ .../simple/SimpleUserService.java | 2 + .../simple/SimpleUserSettingService.java | 14 ++ .../simple/totp/Base32String.java | 145 +++++++++++ .../simple/totp/HexEncoding.java | 56 +++++ .../simple/totp/TotpTwoFactorProvider.java | 63 +++++ .../authorization/simple/totp/TotpUtil.java | 235 ++++++++++++++++++ .../AuthorizationAutoConfiguration.java | 22 +- .../src/main/resources/hsweb-starter.js | 15 +- .../starter/TotpTwoFactorProviderTests.groovy | 37 +++ .../src/test/resources/application.yml | 3 + .../controller/UserSettingController.java | 32 ++- 46 files changed, 1459 insertions(+), 94 deletions(-) create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/NeedTwoFactorException.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingNullValueHolder.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingValueHolder.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/StringSourceSettingHolder.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingManager.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingPermission.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorToken.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorTokenManager.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorProvider.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidator.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorManager.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorProvider.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManager.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/UnsupportedTwoFactorValidator.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManagerTest.java create mode 100644 hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java create mode 100644 hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestTwoFactorValidatorProvider.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/TotpTwoFactorCreatedEvent.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/UserCreatedEvent.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/InServiceUserSettingManager.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/Base32String.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/HexEncoding.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpTwoFactorProvider.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpUtil.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/groovy/org/hswebframework/web/authorization/starter/TotpTwoFactorProviderTests.groovy diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java index e8bfa526b..855e27b6f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java @@ -128,7 +128,6 @@ public interface Authentication extends Serializable { * @param 属性值类型 * @return Optional属性值 */ - @Deprecated Optional getAttribute(String name); /** @@ -139,7 +138,6 @@ public interface Authentication extends Serializable { * @param object 属性值 * @see AuthenticationManager#sync(Authentication) */ - @Deprecated void setAttribute(String name, Serializable object); /** @@ -148,7 +146,6 @@ public interface Authentication extends Serializable { * @param attributes 属性值map * @see AuthenticationManager#sync(Authentication) */ - @Deprecated void setAttributes(Map attributes); /** @@ -159,7 +156,6 @@ public interface Authentication extends Serializable { * @return 被删除的值 * @see AuthenticationManager#sync(Authentication) */ - @Deprecated T removeAttributes(String name); /** @@ -167,7 +163,6 @@ public interface Authentication extends Serializable { * * @return 全部属性集合 */ - @Deprecated Map getAttributes(); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java index 18b439359..491036ddd 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java @@ -107,10 +107,5 @@ public @interface Authorize { */ RequiresDataAccess dataAccess() default @RequiresDataAccess(ignore = true); - /** - * @return 双重验证 - */ - TwoFactor twoFactor() default @TwoFactor(ignore = true); - String[] description() default {}; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java index 747d333fe..2b9596033 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java @@ -7,11 +7,13 @@ import java.lang.annotation.*; @Inherited @Documented public @interface TwoFactor { - String operation() default ""; + String value(); long timeout() default 10 * 60 * 1000L; String provider() default "totp"; + String parameter() default "verifyCode"; + boolean ignore() default false; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java index fef5b0bc8..32cf0316d 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java @@ -1,5 +1,7 @@ package org.hswebframework.web.authorization.exception; +import lombok.Getter; + /** * 权限验证异常 * @@ -10,6 +12,9 @@ public class AccessDenyException extends RuntimeException { private static final long serialVersionUID = -5135300127303801430L; + @Getter + private String code; + public AccessDenyException() { this("权限不足,拒绝访问!"); } @@ -21,4 +26,9 @@ public class AccessDenyException extends RuntimeException { public AccessDenyException(String message, Throwable cause) { super(message, cause); } + + public AccessDenyException(String message, String code, Throwable cause) { + super(message, cause); + this.code = code; + } } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/NeedTwoFactorException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/NeedTwoFactorException.java new file mode 100644 index 000000000..cbe41885f --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/NeedTwoFactorException.java @@ -0,0 +1,19 @@ +package org.hswebframework.web.authorization.exception; + +import lombok.Getter; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Getter +public class NeedTwoFactorException extends RuntimeException { + private static final long serialVersionUID = 3655980280834947633L; + private String provider; + + public NeedTwoFactorException(String message, String provider) { + super(message); + this.provider = provider; + } + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingNullValueHolder.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingNullValueHolder.java new file mode 100644 index 000000000..05a445365 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingNullValueHolder.java @@ -0,0 +1,56 @@ +package org.hswebframework.web.authorization.setting; + +import java.util.List; +import java.util.Optional; + +/** + * @author zhouhao + * @since 1.0.0 + */ +public class SettingNullValueHolder implements SettingValueHolder { + + public static final SettingNullValueHolder INSTANCE = new SettingNullValueHolder(); + + private SettingNullValueHolder() { + } + + @Override + public Optional> asList(Class t) { + return Optional.empty(); + } + + @Override + public Optional as(Class t) { + return Optional.empty(); + } + + @Override + public Optional asString() { + return Optional.empty(); + } + + @Override + public Optional asLong() { + return Optional.empty(); + } + + @Override + public Optional asInt() { + return Optional.empty(); + } + + @Override + public Optional asDouble() { + return Optional.empty(); + } + + @Override + public Optional getValue() { + return Optional.empty(); + } + + @Override + public UserSettingPermission getPermission() { + return UserSettingPermission.NONE; + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingValueHolder.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingValueHolder.java new file mode 100644 index 000000000..6564b1bfe --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingValueHolder.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.authorization.setting; + +import java.util.List; +import java.util.Optional; + +public interface SettingValueHolder { + + SettingValueHolder NULL = SettingNullValueHolder.INSTANCE; + + Optional> asList(Class t); + + Optional as(Class t); + + Optional asString(); + + Optional asLong(); + + Optional asInt(); + + Optional asDouble(); + + Optional getValue(); + + UserSettingPermission getPermission(); + +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/StringSourceSettingHolder.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/StringSourceSettingHolder.java new file mode 100644 index 000000000..af2bbf0ab --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/StringSourceSettingHolder.java @@ -0,0 +1,98 @@ +package org.hswebframework.web.authorization.setting; + + +import com.alibaba.fastjson.JSON; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.utils.StringUtils; +import org.hswebframework.web.dict.EnumDict; + +import java.util.List; +import java.util.Optional; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@AllArgsConstructor +@Getter +public class StringSourceSettingHolder implements SettingValueHolder { + + private String value; + + private UserSettingPermission permission; + + public static SettingValueHolder of(String value, UserSettingPermission permission) { + if (value == null) { + return SettingValueHolder.NULL; + } + return new StringSourceSettingHolder(value, permission); + } + + @Override + public Optional> asList(Class t) { + return getNativeValue() + .map(v -> JSON.parseArray(v, t)); + } + + protected T convert(String value, Class t) { + if (t.isEnum()) { + if (EnumDict.class.isAssignableFrom(t)) { + T val = (T) EnumDict.find((Class) t, value).orElse(null); + if (null != val) { + return val; + } + } + for (T enumConstant : t.getEnumConstants()) { + if (((Enum) enumConstant).name().equalsIgnoreCase(value)) { + return enumConstant; + } + } + } + return JSON.parseObject(value, t); + } + + @Override + @SuppressWarnings("all") + public Optional as(Class t) { + if (t == String.class) { + return (Optional) asString(); + } else if (Long.class == t || long.class == t) { + return (Optional) asLong(); + } else if (Integer.class == t || int.class == t) { + return (Optional) asInt(); + } else if (Double.class == t || double.class == t) { + return (Optional) asDouble(); + } + return getNativeValue().map(v -> convert(v, t)); + } + + @Override + public Optional asString() { + return getNativeValue(); + } + + @Override + public Optional asLong() { + return getNativeValue().map(StringUtils::toLong); + } + + @Override + public Optional asInt() { + return getNativeValue().map(StringUtils::toInt); + } + + @Override + public Optional asDouble() { + return getNativeValue().map(StringUtils::toDouble); + } + + private Optional getNativeValue() { + return Optional.ofNullable(value); + } + + @Override + public Optional getValue() { + return Optional.ofNullable(value); + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingManager.java new file mode 100644 index 000000000..61bd689db --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingManager.java @@ -0,0 +1,13 @@ +package org.hswebframework.web.authorization.setting; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public interface UserSettingManager { + + SettingValueHolder getSetting(String userId, String key); + + void saveSetting(String userId, String key, String value, UserSettingPermission permission); + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingPermission.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingPermission.java new file mode 100644 index 000000000..6127ad402 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingPermission.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.authorization.setting; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.Dict; +import org.hswebframework.web.dict.EnumDict; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@AllArgsConstructor +@Getter +@Dict(id = "user-setting-permission") +public enum UserSettingPermission implements EnumDict { + NONE("无"), + R("读"), + W("写"), + RW("读写"); + private String text; + + @Override + public String getValue() { + return name(); + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java index ec8b1c9b0..3d5aaf32f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java @@ -11,6 +11,8 @@ import org.hswebframework.web.authorization.simple.builder.SimpleDataAccessConfi import org.hswebframework.web.authorization.token.DefaultUserTokenManager; import org.hswebframework.web.authorization.token.UserTokenAuthenticationSupplier; import org.hswebframework.web.authorization.token.UserTokenManager; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; +import org.hswebframework.web.authorization.twofactor.defaults.DefaultTwoFactorValidatorManager; import org.hswebframework.web.convert.CustomMessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -56,6 +58,12 @@ public class DefaultAuthorizationAutoConfiguration { return factory; } + @Bean + @ConditionalOnMissingBean(TwoFactorValidatorManager.class) + public DefaultTwoFactorValidatorManager defaultTwoFactorValidatorManager() { + return new DefaultTwoFactorValidatorManager(); + } + @Bean @ConditionalOnMissingBean(AuthenticationBuilderFactory.class) public AuthenticationBuilderFactory authenticationBuilderFactory(DataAccessConfigBuilderFactory dataAccessConfigBuilderFactory) { diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorToken.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorToken.java new file mode 100644 index 000000000..7586cd695 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorToken.java @@ -0,0 +1,13 @@ +package org.hswebframework.web.authorization.twofactor; + +import java.io.Serializable; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public interface TwoFactorToken extends Serializable { + void generate(long timeout); + + boolean expired(); +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorTokenManager.java new file mode 100644 index 000000000..f4b0fdfcb --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorTokenManager.java @@ -0,0 +1,9 @@ +package org.hswebframework.web.authorization.twofactor; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public interface TwoFactorTokenManager { + TwoFactorToken getToken(String userId, String operation); +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidator.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidator.java index 376bbf674..b639eaa16 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidator.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidator.java @@ -2,9 +2,14 @@ package org.hswebframework.web.authorization.twofactor; /** * 双重验证器,用于某些接口需要双重验证时使用,如: 短信验证码,动态口令等 + * + * @author zhouhao + * @since 3.0.4 */ public interface TwoFactorValidator { + String getProvider(); + /** * 验证code是否有效,如果验证码有效,则保持此验证有效期.在有效期内,调用{@link this#expired()} 将返回false * @@ -20,6 +25,4 @@ public interface TwoFactorValidator { * @return 是否过期 */ boolean expired(); - - } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorManager.java index 87559db34..df736a66e 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorManager.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorManager.java @@ -1,19 +1,18 @@ package org.hswebframework.web.authorization.twofactor; -import org.hswebframework.web.authorization.Authentication; - /** * 双重验证管理器 + * @author zhouhao + * @since 3.0.4 */ public interface TwoFactorValidatorManager { /** * 获取用户使用的双重验证器 * - * @param userId 用户id - * @param operation 进行的操作 + * @param provider 验证器供应商 * @return 验证器 */ - TwoFactorValidator getValidator(String userId, String operation); + TwoFactorValidator getValidator(String userId,String operation, String provider); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorProvider.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorProvider.java new file mode 100644 index 000000000..f89dfe20c --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorProvider.java @@ -0,0 +1,12 @@ +package org.hswebframework.web.authorization.twofactor; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public interface TwoFactorValidatorProvider { + + String getProvider(); + + TwoFactorValidator createTwoFactorValidator(String userId,String operation); +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidator.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidator.java new file mode 100644 index 000000000..345d1b13d --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidator.java @@ -0,0 +1,38 @@ +package org.hswebframework.web.authorization.twofactor.defaults; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.authorization.twofactor.TwoFactorToken; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidator; + +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@AllArgsConstructor +public class DefaultTwoFactorValidator implements TwoFactorValidator { + + @Getter + private String provider; + + private Function validator; + + private Supplier tokenSupplier; + + @Override + public boolean verify(String code, long timeout) { + boolean success = validator.apply(code); + if (success) { + tokenSupplier.get().generate(timeout); + } + return success; + } + + @Override + public boolean expired() { + return tokenSupplier.get().expired(); + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorManager.java new file mode 100644 index 000000000..1001cb5ab --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorManager.java @@ -0,0 +1,47 @@ +package org.hswebframework.web.authorization.twofactor.defaults; + +import org.hswebframework.web.authorization.twofactor.TwoFactorValidator; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorProvider; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public class DefaultTwoFactorValidatorManager implements TwoFactorValidatorManager, BeanPostProcessor { + + private String defaultProvider = "totp"; + + private Map providers = new HashMap<>(); + + @Override + public TwoFactorValidator getValidator(String userId, String operation, String provider) { + if (provider == null) { + provider = defaultProvider; + } + TwoFactorValidatorProvider validatorProvider = providers.get(provider); + if (validatorProvider == null) { + return new UnsupportedTwoFactorValidator(provider); + } + return validatorProvider.createTwoFactorValidator(userId, operation); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof TwoFactorValidatorProvider) { + TwoFactorValidatorProvider provider = ((TwoFactorValidatorProvider) bean); + providers.put(provider.getProvider(), provider); + } + return bean; + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorProvider.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorProvider.java new file mode 100644 index 000000000..b416fc284 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorProvider.java @@ -0,0 +1,30 @@ +package org.hswebframework.web.authorization.twofactor.defaults; + +import lombok.Getter; +import org.hswebframework.web.authorization.twofactor.TwoFactorTokenManager; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidator; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorProvider; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Getter +public abstract class DefaultTwoFactorValidatorProvider implements TwoFactorValidatorProvider { + + private String provider; + + private TwoFactorTokenManager twoFactorTokenManager; + + public DefaultTwoFactorValidatorProvider(String provider, TwoFactorTokenManager twoFactorTokenManager) { + this.provider = provider; + this.twoFactorTokenManager = twoFactorTokenManager; + } + + protected abstract boolean validate(String userId, String code); + + @Override + public TwoFactorValidator createTwoFactorValidator(String userId, String operation) { + return new DefaultTwoFactorValidator(getProvider(), code -> validate(userId, code), () -> twoFactorTokenManager.getToken(userId, operation)); + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManager.java new file mode 100644 index 000000000..663949af7 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManager.java @@ -0,0 +1,70 @@ +package org.hswebframework.web.authorization.twofactor.defaults; + +import lombok.Setter; +import org.hswebframework.web.authorization.twofactor.TwoFactorToken; +import org.hswebframework.web.authorization.twofactor.TwoFactorTokenManager; + +import java.io.Serializable; +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public class HashMapTwoFactorTokenManager implements TwoFactorTokenManager { + + private Map> tokens = new ConcurrentHashMap<>(); + + private class TwoFactorTokenInfo implements Serializable { + private volatile long lastRequestTime = System.currentTimeMillis(); + + private long timeOut; + + private boolean isExpire() { + return System.currentTimeMillis() - lastRequestTime >= timeOut; + } + } + + + private String createTokenInfoKey(String userId, String operation) { + return userId + "_" + operation; + } + + private TwoFactorTokenInfo getTokenInfo(String userId, String operation) { + return Optional.ofNullable(tokens.get(createTokenInfoKey(userId, operation))) + .map(WeakReference::get) + .orElse(null); + } + + @Override + public TwoFactorToken getToken(String userId, String operation) { + + return new TwoFactorToken() { + private static final long serialVersionUID = -5148037320548431456L; + + @Override + public void generate(long timeout) { + TwoFactorTokenInfo info = new TwoFactorTokenInfo(); + info.timeOut = timeout; + tokens.put(createTokenInfoKey(userId, operation), new WeakReference<>(info)); + } + + @Override + public boolean expired() { + TwoFactorTokenInfo info = getTokenInfo(userId, operation); + if (info == null) { + return true; + } + if (info.isExpire()) { + tokens.remove(createTokenInfoKey(userId, operation)); + return true; + } + info.lastRequestTime = System.currentTimeMillis(); + return false; + } + }; + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/UnsupportedTwoFactorValidator.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/UnsupportedTwoFactorValidator.java new file mode 100644 index 000000000..092b25631 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/UnsupportedTwoFactorValidator.java @@ -0,0 +1,26 @@ +package org.hswebframework.web.authorization.twofactor.defaults; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidator; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@AllArgsConstructor +public class UnsupportedTwoFactorValidator implements TwoFactorValidator { + + @Getter + private String provider; + + @Override + public boolean verify(String code, long timeout) { + throw new UnsupportedOperationException("不支持的验证规则:" + provider); + } + + @Override + public boolean expired() { + throw new UnsupportedOperationException("不支持的验证规则:" + provider); + } +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManagerTest.java b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManagerTest.java new file mode 100644 index 000000000..7e39dc7e7 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManagerTest.java @@ -0,0 +1,29 @@ +package org.hswebframework.web.authorization.twofactor.defaults; + +import lombok.SneakyThrows; +import org.hswebframework.web.authorization.twofactor.TwoFactorToken; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author zhouhao + * @since 3.0.4 + */ +public class HashMapTwoFactorTokenManagerTest { + + HashMapTwoFactorTokenManager tokenManager = new HashMapTwoFactorTokenManager(); + + @Test + @SneakyThrows + public void test() { + TwoFactorToken twoFactorToken = tokenManager.getToken("test", "test"); + + Assert.assertTrue(twoFactorToken.expired()); + twoFactorToken.generate(1000L); + Assert.assertFalse(twoFactorToken.expired()); + Thread.sleep(1100); + Assert.assertTrue(twoFactorToken.expired()); + } +} \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AopAuthorizeAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AopAuthorizeAutoConfiguration.java index de238898d..c8c2527c9 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AopAuthorizeAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AopAuthorizeAutoConfiguration.java @@ -31,4 +31,5 @@ public class AopAuthorizeAutoConfiguration { return new AopAuthorizingController(authorizingHandler, aopMethodAuthorizeDefinitionParser); } + } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java index 313c14482..4a3869a34 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java @@ -8,17 +8,19 @@ import org.hswebframework.web.authorization.basic.embed.EmbedAuthenticationManag import org.hswebframework.web.authorization.basic.handler.DefaultAuthorizingHandler; import org.hswebframework.web.authorization.basic.handler.UserAllowPermissionHandler; import org.hswebframework.web.authorization.basic.handler.access.DefaultDataAccessController; +import org.hswebframework.web.authorization.basic.twofactor.TwoFactorHandlerInterceptorAdapter; import org.hswebframework.web.authorization.basic.web.*; import org.hswebframework.web.authorization.basic.web.session.UserTokenAutoExpiredListener; import org.hswebframework.web.authorization.token.UserTokenManager; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @@ -58,6 +60,20 @@ public class AuthorizingHandlerAutoConfiguration { @Bean + @ConditionalOnProperty(prefix = "hsweb.authorize.two-factor", name = "enable", havingValue = "true") + @Order(100) + public WebMvcConfigurer twoFactorHandlerConfigurer(TwoFactorValidatorManager manager) { + return new WebMvcConfigurerAdapter() { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new TwoFactorHandlerInterceptorAdapter(manager)); + super.addInterceptors(registry); + } + }; + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) public WebMvcConfigurer webUserTokenInterceptorConfigurer(UserTokenManager userTokenManager, AopMethodAuthorizeDefinitionParser parser, List userTokenParser) { diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java new file mode 100644 index 000000000..59c667c22 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java @@ -0,0 +1,52 @@ +package org.hswebframework.web.authorization.basic.twofactor; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.authorization.User; +import org.hswebframework.web.authorization.annotation.TwoFactor; +import org.hswebframework.web.authorization.exception.NeedTwoFactorException; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidator; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@AllArgsConstructor +public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapter { + + private TwoFactorValidatorManager validatorManager; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (handler instanceof HandlerMethod) { + HandlerMethod method = ((HandlerMethod) handler); + TwoFactor factor = method.getMethodAnnotation(TwoFactor.class); + if (factor == null || factor.ignore()) { + return true; + } + String userId = Authentication.current() + .map(Authentication::getUser) + .map(User::getId) + .orElse(null); + TwoFactorValidator validator = validatorManager.getValidator(userId, factor.value(), factor.provider()); + if (!validator.expired()) { + return true; + } + String code = request.getParameter(factor.parameter()); + if (StringUtils.isEmpty(code)) { + throw new NeedTwoFactorException("需要进行双重验证", factor.provider()); + } else if (!validator.verify(code, factor.timeout())) { + throw new NeedTwoFactorException("验证码错误", factor.provider()); + } + } + return super.preHandle(request, response, handler); + } +} diff --git a/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/FullFunctionTest.groovy b/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/FullFunctionTest.groovy index ac01c8a56..c17273084 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/FullFunctionTest.groovy +++ b/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/FullFunctionTest.groovy @@ -51,6 +51,29 @@ class FullFunctionTest extends Specification { } + def "测试双重验证"() { + given: "登录" + def token = doLogin("admin", "admin") + when: "登录成功" + token != null + then: "调用双重验证接口" + mockMvc.perform(get("/test/two-factor") + .header("token", token)) + .andExpect(status().is(403)) + .andReturn() + .getResponse() + .getContentAsString() + def resp = mockMvc.perform(get("/test/two-factor") + .header("token", token) + .param("verifyCode", "test")) + .andExpect(status().is(200)) + .andReturn() + .getResponse() + .getContentAsString() + expect: + resp != null + } + def "测试查询"() { given: "登录" def token = doLogin("admin", "admin") diff --git a/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestCrudController.java b/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestCrudController.java index 049137776..55f8fcb06 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestCrudController.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestCrudController.java @@ -1,8 +1,10 @@ package org.hswebframework.web.authorization.full.controller; import org.hswebframework.web.authorization.annotation.Authorize; +import org.hswebframework.web.authorization.annotation.TwoFactor; import org.hswebframework.web.authorization.full.controller.model.TestModel; import org.hswebframework.web.controller.message.ResponseMessage; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,4 +23,10 @@ public class TestCrudController implements CrudController { return ResponseMessage.ok(); } + + @TwoFactor(value = "test", provider = "test") + @GetMapping("/two-factor") + public ResponseMessage testTowFactor() { + return ResponseMessage.ok(); + } } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestTwoFactorValidatorProvider.java b/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestTwoFactorValidatorProvider.java new file mode 100644 index 000000000..d8fcf906c --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestTwoFactorValidatorProvider.java @@ -0,0 +1,39 @@ +package org.hswebframework.web.authorization.full.controller; + +import org.hswebframework.web.authorization.twofactor.TwoFactorValidator; +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorProvider; +import org.springframework.stereotype.Component; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Component +public class TestTwoFactorValidatorProvider implements TwoFactorValidatorProvider { + @Override + public String getProvider() { + return "test"; + } + + @Override + public TwoFactorValidator createTwoFactorValidator(String userId, String operation) { + return new TwoFactorValidator() { + boolean success = false; + + @Override + public String getProvider() { + return "test"; + } + + @Override + public boolean verify(String code, long timeout) { + return success = code.equalsIgnoreCase("test"); + } + + @Override + public boolean expired() { + return !success; + } + }; + } +} diff --git a/hsweb-authorization/hsweb-authorization-basic/src/test/resources/application.yml b/hsweb-authorization/hsweb-authorization-basic/src/test/resources/application.yml index e5eac691d..cd93127d7 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/test/resources/application.yml +++ b/hsweb-authorization/hsweb-authorization-basic/src/test/resources/application.yml @@ -1,59 +1,60 @@ - spring: - aop: - auto: true - proxy-target-class: true - datasource: - url : jdbc:h2:mem:example-oauth2-client - username : sa - password : - type: com.alibaba.druid.pool.DruidDataSource - driver-class-name : org.h2.Driver - cache: - type: simple + aop: + auto: true + proxy-target-class: true + datasource: + url: jdbc:h2:mem:example-oauth2-client + username: sa + password: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: org.h2.Driver + cache: + type: simple hsweb: - app: - name: hsweb-oauth2 客户端示例 - version: 3.0.0 - authorize: - allows: - users: - admin: "**.TestController.*" - users: - admin: - name: 超级管理员 - username: admin - password: admin - roles: #用户的角色 - - id: admin - name: 管理员 - - id: user - name: 用户 - permissions-simple: - test: query,get - permissions: - - id: user-manager - actions: query,get,update,delete - dataAccesses: - - action: query - type: DENY_FIELDS - fields: - - password - - salt - - id: test - actions: query,add,update - dataAccesses: - - action: query - type: DENY_FIELDS - fields: - - password - - action: update - type: DENY_FIELDS - fields: - - name - - action: add - type: DENY_FIELDS - fields: - - id + app: + name: hsweb-oauth2 客户端示例 + version: 3.0.0 + authorize: + allows: + users: + admin: "**.TestController.*" + two-factor: + enable: true + users: + admin: + name: 超级管理员 + username: admin + password: admin + roles: #用户的角色 + - id: admin + name: 管理员 + - id: user + name: 用户 + permissions-simple: + test: query,get + permissions: + - id: user-manager + actions: query,get,update,delete + dataAccesses: + - action: query + type: DENY_FIELDS + fields: + - password + - salt + - id: test + actions: query,add,update + dataAccesses: + - action: query + type: DENY_FIELDS + fields: + - password + - action: update + type: DENY_FIELDS + fields: + - name + - action: add + type: DENY_FIELDS + fields: + - id server: port: 8808 diff --git a/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/RestControllerExceptionTranslator.java b/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/RestControllerExceptionTranslator.java index 0a73407e9..f2e24ba9f 100644 --- a/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/RestControllerExceptionTranslator.java +++ b/hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/RestControllerExceptionTranslator.java @@ -21,6 +21,7 @@ import com.alibaba.fastjson.JSONException; import org.hswebframework.web.BusinessException; import org.hswebframework.web.NotFoundException; import org.hswebframework.web.authorization.exception.AccessDenyException; +import org.hswebframework.web.authorization.exception.NeedTwoFactorException; import org.hswebframework.web.authorization.exception.UnAuthorizedException; import org.hswebframework.web.controller.message.ResponseMessage; import org.hswebframework.web.validate.SimpleValidateResults; @@ -196,6 +197,15 @@ public class RestControllerExceptionTranslator { return ResponseMessage.error(400, msg); } + @ExceptionHandler(NeedTwoFactorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + ResponseMessage handleException(NeedTwoFactorException e) { + return ResponseMessage + .error(403, e.getMessage()) + .code("need_tow_factor") + .result(e.getProvider()); + } + /** * 请求方式不支持异常 * 比如:POST方式的API, GET方式请求 diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/UserSettingEntity.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/UserSettingEntity.java index 5b9a1262a..93780c75a 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/UserSettingEntity.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/UserSettingEntity.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.NotBlank; +import org.hswebframework.web.authorization.setting.UserSettingPermission; import org.hswebframework.web.commons.entity.SimpleGenericEntity; import org.hswebframework.web.validator.group.CreateGroup; @@ -21,7 +22,6 @@ public class UserSettingEntity extends SimpleGenericEntity { private String userId; @NotBlank(groups = CreateGroup.class) - private String key; @NotBlank(groups = CreateGroup.class) @@ -38,4 +38,17 @@ public class UserSettingEntity extends SimpleGenericEntity { private Date updateTime; + private UserSettingPermission permission; + + public boolean hasPermission(UserSettingPermission... permissions) { + if (permission == null) { + return true; + } + if (permission == UserSettingPermission.NONE) { + return false; + } + + return permission.in(permissions); + + } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/UserService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/UserService.java index 52b0f0f9c..0671c147f 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/UserService.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/UserService.java @@ -23,6 +23,17 @@ public interface UserService extends QueryService, InsertService { + /** + * 新增用户 + * + * @param data 要添加的数据 + * @return 用户id + * @see org.hswebframework.web.service.authorization.events.UserCreatedEvent + * @see BindRoleUserEntity + */ + @Override + String insert(UserEntity data); + /** * 启用用户 * diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/TotpTwoFactorCreatedEvent.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/TotpTwoFactorCreatedEvent.java new file mode 100644 index 000000000..271a78e3a --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/TotpTwoFactorCreatedEvent.java @@ -0,0 +1,18 @@ +package org.hswebframework.web.service.authorization.events; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.entity.authorization.UserEntity; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Getter +@AllArgsConstructor +public class TotpTwoFactorCreatedEvent { + private UserEntity userEntity; + + private String totpUrl; +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/UserCreatedEvent.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/UserCreatedEvent.java new file mode 100644 index 000000000..238d4bf3f --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/UserCreatedEvent.java @@ -0,0 +1,16 @@ +package org.hswebframework.web.service.authorization.events; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.entity.authorization.UserEntity; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Getter +@AllArgsConstructor +public class UserCreatedEvent { + UserEntity userEntity; +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/InServiceUserSettingManager.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/InServiceUserSettingManager.java new file mode 100644 index 000000000..56ae3d2c4 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/InServiceUserSettingManager.java @@ -0,0 +1,41 @@ +package org.hswebframework.web.service.authorization.simple; + +import org.hswebframework.web.authorization.setting.SettingValueHolder; +import org.hswebframework.web.authorization.setting.StringSourceSettingHolder; +import org.hswebframework.web.authorization.setting.UserSettingManager; +import org.hswebframework.web.authorization.setting.UserSettingPermission; +import org.hswebframework.web.entity.authorization.UserSettingEntity; +import org.hswebframework.web.service.authorization.UserSettingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Service +public class InServiceUserSettingManager implements UserSettingManager { + + @Autowired + private UserSettingService userSettingService; + + @Override + public SettingValueHolder getSetting(String userId, String key) { + UserSettingEntity entity = userSettingService.selectByUser(userId, "user-setting", key); + if (entity == null) { + return SettingValueHolder.NULL; + } + return StringSourceSettingHolder.of(entity.getSetting(), entity.getPermission()); + } + + @Override + public void saveSetting(String userId, String key, String value, UserSettingPermission permission) { + UserSettingEntity entity = new UserSettingEntity(); + entity.setUserId(userId); + entity.setKey("user-setting"); + entity.setSettingId(key); + entity.setSetting(value); + entity.setPermission(permission); + userSettingService.saveOrUpdate(entity); + } +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserService.java index db8582aa1..0868311eb 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserService.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserService.java @@ -15,6 +15,7 @@ import org.hswebframework.web.id.IDGenerator; import org.hswebframework.web.service.AbstractService; import org.hswebframework.web.service.DefaultDSLQueryService; import org.hswebframework.web.service.authorization.events.ClearUserAuthorizationCacheEvent; +import org.hswebframework.web.service.authorization.events.UserCreatedEvent; import org.hswebframework.web.service.authorization.events.UserModifiedEvent; import org.hswebframework.web.service.authorization.simple.terms.UserInRoleSqlTerm; import org.hswebframework.web.validate.ValidationException; @@ -156,6 +157,7 @@ public class SimpleUserService extends AbstractService trySyncUserRole(userEntity.getId(), bindRoleUserEntity.getRoles()); } } + publisher.publishEvent(new UserCreatedEvent(userEntity)); return userEntity.getId(); } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserSettingService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserSettingService.java index a416aa2d7..81fe56180 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserSettingService.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserSettingService.java @@ -38,6 +38,20 @@ public class SimpleUserSettingService extends EnableCacheGenericEntityService selectByUser(String userId, String key) { diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/Base32String.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/Base32String.java new file mode 100644 index 000000000..1580e4cfb --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/Base32String.java @@ -0,0 +1,145 @@ +package org.hswebframework.web.service.authorization.simple.totp; /** + */ +import java.util.HashMap; +import java.util.Locale; + +/** + * Encodes arbitrary byte arrays as case-insensitive base-32 strings. + *

+ * The implementation is slightly different than in RFC 4648. During encoding, + * padding is not added, and during decoding the last incomplete chunk is not + * taken into account. The result is that multiple strings decode to the same + * byte array, for example, string of sixteen 7s ("7...7") and seventeen 7s both + * decode to the same byte array. + * TODO(sarvar): Revisit this encoding and whether this ambiguity needs fixing. + * + * @author sweis@google.com (Steve Weis) + * @author Neal Gafter + */ +public class Base32String { + // singleton + + private static final Base32String INSTANCE = + new Base32String("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); // RFC 4648/3548 + + static Base32String getInstance() { + return INSTANCE; + } + + // 32 alpha-numeric characters. + private String ALPHABET; + private char[] DIGITS; + private int MASK; + private int SHIFT; + private HashMap CHAR_MAP; + + static final String SEPARATOR = "-"; + + protected Base32String(String alphabet) { + this.ALPHABET = alphabet; + DIGITS = ALPHABET.toCharArray(); + MASK = DIGITS.length - 1; + SHIFT = Integer.numberOfTrailingZeros(DIGITS.length); + CHAR_MAP = new HashMap<>(); + for (int i = 0; i < DIGITS.length; i++) { + CHAR_MAP.put(DIGITS[i], i); + } + } + + public static byte[] decode(String encoded) throws DecodingException { + return getInstance().decodeInternal(encoded); + } + + protected byte[] decodeInternal(String encoded) throws DecodingException { + // Remove whitespace and separators + encoded = encoded.trim().replaceAll(SEPARATOR, "").replaceAll(" ", ""); + + // Remove padding. Note: the padding is used as hint to determine how many + // bits to decode from the last incomplete chunk (which is commented out + // below, so this may have been wrong to start with). + encoded = encoded.replaceFirst("[=]*$", ""); + + // Canonicalize to all upper case + encoded = encoded.toUpperCase(Locale.US); + if (encoded.length() == 0) { + return new byte[0]; + } + int encodedLength = encoded.length(); + int outLength = encodedLength * SHIFT / 8; + byte[] result = new byte[outLength]; + int buffer = 0; + int next = 0; + int bitsLeft = 0; + for (char c : encoded.toCharArray()) { + if (!CHAR_MAP.containsKey(c)) { + throw new DecodingException("Illegal character: " + c); + } + buffer <<= SHIFT; + buffer |= CHAR_MAP.get(c) & MASK; + bitsLeft += SHIFT; + if (bitsLeft >= 8) { + result[next++] = (byte) (buffer >> (bitsLeft - 8)); + bitsLeft -= 8; + } + } + // We'll ignore leftover bits for now. + // + // if (next != outLength || bitsLeft >= SHIFT) { + // throw new DecodingException("Bits left: " + bitsLeft); + // } + return result; + } + + public static String encode(byte[] data) { + return getInstance().encodeInternal(data); + } + + protected String encodeInternal(byte[] data) { + if (data.length == 0) { + return ""; + } + + // SHIFT is the number of bits per output character, so the length of the + // output is the length of the input multiplied by 8/SHIFT, rounded up. + if (data.length >= (1 << 28)) { + // The computation below will fail, so don't do it. + throw new IllegalArgumentException(); + } + + int outputLength = (data.length * 8 + SHIFT - 1) / SHIFT; + StringBuilder result = new StringBuilder(outputLength); + + int buffer = data[0]; + int next = 1; + int bitsLeft = 8; + while (bitsLeft > 0 || next < data.length) { + if (bitsLeft < SHIFT) { + if (next < data.length) { + buffer <<= 8; + buffer |= (data[next++] & 0xff); + bitsLeft += 8; + } else { + int pad = SHIFT - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + int index = MASK & (buffer >> (bitsLeft - SHIFT)); + bitsLeft -= SHIFT; + result.append(DIGITS[index]); + } + return result.toString(); + } + + @Override + // enforce that this class is a singleton + public Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + public static class DecodingException extends Exception { + public DecodingException(String message) { + super(message); + } + } +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/HexEncoding.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/HexEncoding.java new file mode 100644 index 000000000..417da77a8 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/HexEncoding.java @@ -0,0 +1,56 @@ +package org.hswebframework.web.service.authorization.simple.totp; + +public class HexEncoding { + + /** Hidden constructor to prevent instantiation. */ + private HexEncoding() {} + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + + /** + * Encodes the provided data as a hexadecimal string. + */ + public static String encode(byte[] data) { + StringBuilder result = new StringBuilder(data.length * 2); + for (byte b : data) { + result.append(HEX_DIGITS[(b >>> 4) & 0x0f]); + result.append(HEX_DIGITS[b & 0x0f]); + } + return result.toString(); + } + + /** + * Decodes the provided hexadecimal string into an array of bytes. + */ + public static byte[] decode(String encoded) { + // IMPLEMENTATION NOTE: Special care is taken to permit odd number of hexadecimal digits. + int resultLengthBytes = (encoded.length() + 1) / 2; + byte[] result = new byte[resultLengthBytes]; + int resultOffset = 0; + int encodedCharOffset = 0; + if ((encoded.length() % 2) != 0) { + // Odd number of digits -- the first digit is the lower 4 bits of the first result byte. + result[resultOffset++] = (byte) getHexadecimalDigitValue(encoded.charAt(encodedCharOffset)); + encodedCharOffset++; + } + for (int len = encoded.length(); encodedCharOffset < len; encodedCharOffset += 2) { + result[resultOffset++] = (byte) + ((getHexadecimalDigitValue(encoded.charAt(encodedCharOffset)) << 4) + | getHexadecimalDigitValue(encoded.charAt(encodedCharOffset + 1))); + } + return result; + } + + private static int getHexadecimalDigitValue(char c) { + if ((c >= 'a') && (c <= 'f')) { + return (c - 'a') + 0x0a; + } else if ((c >= 'A') && (c <= 'F')) { + return (c - 'A') + 0x0a; + } else if ((c >= '0') && (c <= '9')) { + return c - '0'; + } else { + throw new IllegalArgumentException( + "Invalid hexadecimal digit at position : '" + c + "' (0x" + Integer.toHexString(c) + ")"); + } + } +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpTwoFactorProvider.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpTwoFactorProvider.java new file mode 100644 index 000000000..900e1b86e --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpTwoFactorProvider.java @@ -0,0 +1,63 @@ +package org.hswebframework.web.service.authorization.simple.totp; + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.authorization.setting.UserSettingManager; +import org.hswebframework.web.authorization.setting.UserSettingPermission; +import org.hswebframework.web.authorization.twofactor.defaults.DefaultTwoFactorValidatorProvider; +import org.hswebframework.web.authorization.twofactor.TwoFactorTokenManager; +import org.hswebframework.web.entity.authorization.UserEntity; +import org.hswebframework.web.service.authorization.events.TotpTwoFactorCreatedEvent; +import org.hswebframework.web.service.authorization.events.UserCreatedEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.transaction.annotation.Transactional; + +import java.util.function.Function; + +/** + * @author zhouhao + * @since 3.0.4 + */ +@Transactional(rollbackFor = Exception.class) +public class TotpTwoFactorProvider extends DefaultTwoFactorValidatorProvider { + + private UserSettingManager userSettingManager; + + @Getter + @Setter + private String domain = "hsweb.me"; + + @Getter + @Setter + private String settingId = "tow-factor-totp-key"; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + public TotpTwoFactorProvider(UserSettingManager userSettingManager, TwoFactorTokenManager twoFactorTokenManager) { + super("totp", twoFactorTokenManager); + this.userSettingManager = userSettingManager; + } + + @EventListener + public void handleUserCreatedEvent(UserCreatedEvent event) { + //生成totp + String key = TotpUtil.getRandomSecretBase32(64); + UserEntity userEntity = event.getUserEntity(); + String keyUrl = TotpUtil.generateTotpString(userEntity.getUsername(), domain, key); + //创建一个用户没有操作权限的配置 + userSettingManager.saveSetting(userEntity.getId(), settingId, key, UserSettingPermission.NONE); + eventPublisher.publishEvent(new TotpTwoFactorCreatedEvent(userEntity, keyUrl)); + } + + @Override + protected boolean validate(String userId, String code) { + return userSettingManager.getSetting(userId, settingId) + .asString() + .map(key -> TotpUtil.verify(key, code)) + .orElse(false); + } + +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpUtil.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpUtil.java new file mode 100644 index 000000000..2b7862d6a --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpUtil.java @@ -0,0 +1,235 @@ +package org.hswebframework.web.service.authorization.simple.totp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.lang.reflect.UndeclaredThrowableException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + */ +public class TotpUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(TotpUtil.class); + + private TotpUtil() { + } + + /** + * This method uses the JCE to provide the crypto algorithm. + * HMAC computes a Hashed Message Authentication Code with the + * crypto hash algorithm as a parameter. + * + * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256, + * HmacSHA512) + * @param keyBytes: the bytes to use for the HMAC key + * @param text: the message or text to be authenticated + */ + private static byte[] hmac_sha(String crypto, byte[] keyBytes, + byte[] text) { + try { + Mac hmac; + hmac = Mac.getInstance(crypto); + SecretKeySpec macKey = + new SecretKeySpec(keyBytes, "RAW"); + hmac.init(macKey); + return hmac.doFinal(text); + } catch (GeneralSecurityException gse) { + throw new UndeclaredThrowableException(gse); + } + } + + /** + * This method converts a HEX string to Byte[] + * + * @param hex: the HEX string + * @return: a byte array + */ + private static byte[] hexStr2Bytes(String hex) { + // Adding one byte to get the right conversion + // Values starting with "0" can be converted + byte[] bArray = new BigInteger("10" + hex, 16).toByteArray(); + + // Copy all the REAL bytes, not the "first" + byte[] ret = new byte[bArray.length - 1]; + for (int i = 0; i < ret.length; i++) + ret[i] = bArray[i + 1]; + return ret; + } + + private static final int[] DIGITS_POWER + // 0 1 2 3 4 5 6 7 8 + = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * @return: a numeric String in base 10 that includes truncationDigits digits + */ + public static String generateTOTP(String key, + String time, + String returnDigits) { + return generateTOTP(key, time, returnDigits, "HmacSHA1"); + } + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * @return: a numeric String in base 10 that includes truncationDigits digits + */ + public static String generateTOTP256(String key, + String time, + String returnDigits) { + return generateTOTP(key, time, returnDigits, "HmacSHA256"); + } + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * @return: a numeric String in base 10 that includes truncationDigits digits + */ + public static String generateTOTP512(String key, + String time, + String returnDigits) { + return generateTOTP(key, time, returnDigits, "HmacSHA512"); + } + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * @param crypto: the crypto function to use + * @return: a numeric String in base 10 that includes truncationDigits digits + */ + public static String generateTOTP(String key, + String time, + String returnDigits, + String crypto) { + int codeDigits = Integer.decode(returnDigits); + StringBuilder result; + + // Using the counter + // First 8 bytes are for the movingFactor + // Compliant with base RFC 4226 (HOTP) + StringBuilder timeBuilder = new StringBuilder(time); + while (timeBuilder.length() < 16) + timeBuilder.insert(0, "0"); + time = timeBuilder.toString(); + + // Get the HEX in a Byte[] + byte[] msg = hexStr2Bytes(time); + byte[] k = hexStr2Bytes(key); + + byte[] hash = hmac_sha(crypto, k, msg); + + // put selected bytes into result int + int offset = hash[hash.length - 1] & 0xf; + + int binary = + ((hash[offset] & 0x7f) << 24) | + ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | + (hash[offset + 3] & 0xff); + + int otp = binary % DIGITS_POWER[codeDigits]; + + result = new StringBuilder(Integer.toString(otp)); + while (result.length() < codeDigits) { + result.insert(0, "0"); + } + return result.toString(); + } + + /** + * 验证动态口令是否正确 + * + * @param secretBase32 密钥 + * @param code 待验证的动态口令 + * @return + */ + public static boolean verify(String secretBase32, String code) { + return generate(secretBase32).equals(code); + } + + /** + * 生成totp协议字符串 + * + * @param accoName + * @param domain + * @param secretBase32 + * @return + */ + public static String generateTotpString(String accoName, String domain, String secretBase32) { + return "otpauth://totp/" + accoName + "@" + domain + "?secret=" + secretBase32; + } + + /** + * 根据密钥生成动态口令 + * + * @param secretBase32 base32编码格式的密钥 + * @return + */ + public static String generate(String secretBase32) { + + String secretHex; + try { + secretHex = HexEncoding.encode(Base32String.decode(secretBase32)); + } catch (Base32String.DecodingException e) { + LOGGER.error("解码" + secretBase32 + "出错,", e); + throw new RuntimeException("解码Base32出错"); + } + + long X = 30; + + StringBuilder steps; + DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + + long currentTime = System.currentTimeMillis() / 1000L; + try { + long t = currentTime / X; + steps = new StringBuilder(Long.toHexString(t).toUpperCase()); + while (steps.length() < 16) steps.insert(0, "0"); + + return generateTOTP(secretHex, steps.toString(), "6", + "HmacSHA1"); + } catch (final Exception e) { + LOGGER.error("生成动态口令出错:" + secretBase32, e); + throw new RuntimeException("生成动态口令出错"); + } + } + + /** + * 生成base32编码的随机密钥 + * + * @param length + * @return + */ + public static String getRandomSecretBase32(int length) { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[length / 2]; + random.nextBytes(salt); + return Base32String.encode(salt); + } +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/java/org/hswebframework/web/authorization/starter/AuthorizationAutoConfiguration.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/java/org/hswebframework/web/authorization/starter/AuthorizationAutoConfiguration.java index fbdf3769d..10be723b2 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/java/org/hswebframework/web/authorization/starter/AuthorizationAutoConfiguration.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/java/org/hswebframework/web/authorization/starter/AuthorizationAutoConfiguration.java @@ -21,13 +21,16 @@ package org.hswebframework.web.authorization.starter; import org.hswebframework.web.authorization.AuthenticationInitializeService; import org.hswebframework.web.authorization.AuthenticationManager; import org.hswebframework.web.authorization.basic.embed.EmbedAuthenticationManager; +import org.hswebframework.web.authorization.setting.UserSettingManager; import org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration; +import org.hswebframework.web.authorization.twofactor.TwoFactorTokenManager; +import org.hswebframework.web.authorization.twofactor.defaults.HashMapTwoFactorTokenManager; import org.hswebframework.web.service.authorization.simple.SimpleAuthenticationManager; +import org.hswebframework.web.service.authorization.simple.totp.TotpTwoFactorProvider; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -78,4 +81,17 @@ public class AuthorizationAutoConfiguration { return new AutoSyncPermission(); } + @Bean + @ConditionalOnMissingBean(TwoFactorTokenManager.class) + public TwoFactorTokenManager twoFactorTokenManager() { + return new HashMapTwoFactorTokenManager(); + } + + @Bean + @ConditionalOnProperty(prefix = "hsweb.authorize.two-factor.totp", name = "enable", havingValue = "true") + @ConfigurationProperties(prefix = "hsweb.authorize.two-factor.totp") + public TotpTwoFactorProvider totpTwoFactorProvider(UserSettingManager userSettingManager, + TwoFactorTokenManager twoFactorTokenManager) { + return new TotpTwoFactorProvider(userSettingManager, twoFactorTokenManager); + } } diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/resources/hsweb-starter.js b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/resources/hsweb-starter.js index db8d5fee6..6b14ff3dc 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/resources/hsweb-starter.js +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/resources/hsweb-starter.js @@ -26,12 +26,15 @@ var info = { //版本更新信息 var versions = [ - // { - // version: "3.0.0", - // upgrade: function (context) { - // java.lang.System.out.println("更新到3.0.2了"); - // } - // } + { + version: "3.0.4", + upgrade: function (context) { + var database = context.database; + database.createOrAlter("s_user_setting") + .addColumn().name("permission").varchar(32).comment("用户可操作权限").commit() + .commit(); + } + } ]; var JDBCType = java.sql.JDBCType; diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/groovy/org/hswebframework/web/authorization/starter/TotpTwoFactorProviderTests.groovy b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/groovy/org/hswebframework/web/authorization/starter/TotpTwoFactorProviderTests.groovy new file mode 100644 index 000000000..e0f0bc9af --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/groovy/org/hswebframework/web/authorization/starter/TotpTwoFactorProviderTests.groovy @@ -0,0 +1,37 @@ +package org.hswebframework.web.authorization.starter + +import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager +import org.hswebframework.web.entity.authorization.SimpleUserEntity +import org.hswebframework.web.service.authorization.UserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author zhouhao + * @since 3.0.4 + */ +@ContextConfiguration +@SpringBootTest(classes = [TestApplication.class], properties = ["classpath:application.yml"]) +class TotpTwoFactorProviderTests extends Specification { + + @Autowired + TwoFactorValidatorManager validatorManager; + + @Autowired + private UserService userService; + + + def "测试totp"() { + given: + String id = userService.insert(new SimpleUserEntity( + username: "admin", + password: "admin", + name: "admin" + )) + expect: + !validatorManager.getValidator(id, "", "totp") + .verify("test", 100) + } +} diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/resources/application.yml b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/resources/application.yml index 42cd5052a..7db45d9d7 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/resources/application.yml +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/resources/application.yml @@ -14,6 +14,9 @@ hsweb: authorize: sync: false auto-parse: false + two-factor: + totp: + enable: true users: fix-bug-91-in-yml: username: "fix-bug-91-in-yml" diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-web/src/main/java/org/hswebframework/web/authorization/controller/UserSettingController.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-web/src/main/java/org/hswebframework/web/authorization/controller/UserSettingController.java index 118d9d1c1..92083fdeb 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-web/src/main/java/org/hswebframework/web/authorization/controller/UserSettingController.java +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-web/src/main/java/org/hswebframework/web/authorization/controller/UserSettingController.java @@ -4,6 +4,8 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.annotation.Authorize; +import org.hswebframework.web.authorization.exception.AccessDenyException; +import org.hswebframework.web.authorization.setting.UserSettingPermission; import org.hswebframework.web.controller.message.ResponseMessage; import org.hswebframework.web.entity.authorization.UserSettingEntity; import org.hswebframework.web.service.authorization.UserSettingService; @@ -12,6 +14,9 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.stream.Collectors; + +import static org.hswebframework.web.authorization.setting.UserSettingPermission.*; /** * @author zhouhao @@ -32,31 +37,44 @@ public class UserSettingController { public ResponseMessage get(Authentication authentication, @PathVariable String key, @PathVariable String id) { - return ResponseMessage.ok(userSettingService.selectByUser(authentication.getUser().getId(), key, id)); + UserSettingEntity entity = userSettingService.selectByUser(authentication.getUser().getId(), key, id); + if (entity != null && entity.hasPermission(R, RW)) { + return ResponseMessage.ok(); + } + return ResponseMessage.ok(); } @GetMapping("/me/{key}") @Authorize(merge = false) @ApiOperation("获取当前用户的配置列表") public ResponseMessage> get(Authentication authentication, - @PathVariable String key) { - return ResponseMessage.ok(userSettingService.selectByUser(authentication.getUser().getId(), key)); + @PathVariable String key) { + + return ResponseMessage.ok(userSettingService + .selectByUser(authentication.getUser().getId(), key) + .stream() + .filter(setting -> setting.hasPermission(R, RW)) + .collect(Collectors.toList())); } @PatchMapping("/me/{key}") @Authorize(merge = false) - @ApiOperation("获取当前用户的配置列表") + @ApiOperation("保存当前用户配置") public ResponseMessage save(Authentication authentication, - @PathVariable String key, - @Validated - @RequestBody UserSettingEntity userSettingEntity) { + @PathVariable String key, + @Validated + @RequestBody UserSettingEntity userSettingEntity) { userSettingEntity.setId(null); userSettingEntity.setUserId(authentication.getUser().getId()); userSettingEntity.setKey(key); UserSettingEntity old = userSettingService.selectByUser(authentication.getUser().getId(), key, userSettingEntity.getSettingId()); if (old != null) { userSettingEntity.setId(old.getId()); + if (!old.hasPermission(RW, R)) { + throw new AccessDenyException("没有权限保存此配置"); + } } + userSettingEntity.setPermission(RW); String id = userSettingService.saveOrUpdate(userSettingEntity); return ResponseMessage.ok(id); }