Browse Source

#103 初步完成双重验证

zhouhao 6 years ago
parent
commit
768033f221
46 changed files with 1459 additions and 94 deletions
  1. 0 5
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java
  2. 0 5
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/Authorize.java
  3. 3 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java
  4. 10 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java
  5. 19 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/NeedTwoFactorException.java
  6. 56 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingNullValueHolder.java
  7. 26 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/SettingValueHolder.java
  8. 98 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/StringSourceSettingHolder.java
  9. 13 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingManager.java
  10. 26 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/setting/UserSettingPermission.java
  11. 8 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java
  12. 13 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorToken.java
  13. 9 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorTokenManager.java
  14. 5 2
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidator.java
  15. 4 5
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorManager.java
  16. 12 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/TwoFactorValidatorProvider.java
  17. 38 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidator.java
  18. 47 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorManager.java
  19. 30 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/DefaultTwoFactorValidatorProvider.java
  20. 70 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManager.java
  21. 26 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/twofactor/defaults/UnsupportedTwoFactorValidator.java
  22. 29 0
      hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/twofactor/defaults/HashMapTwoFactorTokenManagerTest.java
  23. 1 0
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AopAuthorizeAutoConfiguration.java
  24. 20 4
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/configuration/AuthorizingHandlerAutoConfiguration.java
  25. 52 0
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java
  26. 23 0
      hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/FullFunctionTest.groovy
  27. 8 0
      hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestCrudController.java
  28. 39 0
      hsweb-authorization/hsweb-authorization-basic/src/test/groovy/org/hswebframework/web/authorization/full/controller/TestTwoFactorValidatorProvider.java
  29. 56 55
      hsweb-authorization/hsweb-authorization-basic/src/test/resources/application.yml
  30. 10 0
      hsweb-starter/hsweb-spring-boot-starter/src/main/java/org/hswebframework/web/starter/RestControllerExceptionTranslator.java
  31. 14 1
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/entity/authorization/UserSettingEntity.java
  32. 11 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/UserService.java
  33. 18 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/TotpTwoFactorCreatedEvent.java
  34. 16 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/service/authorization/events/UserCreatedEvent.java
  35. 41 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/InServiceUserSettingManager.java
  36. 2 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserService.java
  37. 14 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/SimpleUserSettingService.java
  38. 145 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/Base32String.java
  39. 56 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/HexEncoding.java
  40. 63 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpTwoFactorProvider.java
  41. 235 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-local/src/main/java/org/hswebframework/web/service/authorization/simple/totp/TotpUtil.java
  42. 19 3
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/java/org/hswebframework/web/authorization/starter/AuthorizationAutoConfiguration.java
  43. 9 6
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/main/resources/hsweb-starter.js
  44. 37 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/groovy/org/hswebframework/web/authorization/starter/TotpTwoFactorProviderTests.groovy
  45. 3 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-starter/src/test/resources/application.yml
  46. 25 7
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-web/src/main/java/org/hswebframework/web/authorization/controller/UserSettingController.java

+ 0 - 5
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/Authentication.java

@@ -128,7 +128,6 @@ public interface Authentication extends Serializable {
      * @param <T>  属性值类型
      * @return Optional属性值
      */
-    @Deprecated
     <T extends Serializable> Optional<T> 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<String, Serializable> attributes);
 
     /**
@@ -159,7 +156,6 @@ public interface Authentication extends Serializable {
      * @return 被删除的值
      * @see AuthenticationManager#sync(Authentication)
      */
-    @Deprecated
     <T extends Serializable> T removeAttributes(String name);
 
     /**
@@ -167,7 +163,6 @@ public interface Authentication extends Serializable {
      *
      * @return 全部属性集合
      */
-    @Deprecated
     Map<String, Serializable> getAttributes();
 
 }

+ 0 - 5
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 {};
 }

+ 3 - 1
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;
 }

+ 10 - 0
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;
+    }
 }

+ 19 - 0
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;
+    }
+
+}

+ 56 - 0
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 <T> Optional<List<T>> asList(Class<T> t) {
+        return Optional.empty();
+    }
+
+    @Override
+    public <T> Optional<T> as(Class<T> t) {
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<String> asString() {
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Long> asLong() {
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Integer> asInt() {
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Double> asDouble() {
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Object> getValue() {
+        return Optional.empty();
+    }
+
+    @Override
+    public UserSettingPermission getPermission() {
+        return UserSettingPermission.NONE;
+    }
+}

+ 26 - 0
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;
+
+    <T> Optional<List<T>> asList(Class<T> t);
+
+    <T> Optional<T> as(Class<T> t);
+
+    Optional<String> asString();
+
+    Optional<Long> asLong();
+
+    Optional<Integer> asInt();
+
+    Optional<Double> asDouble();
+
+    Optional<Object> getValue();
+
+    UserSettingPermission getPermission();
+
+}

+ 98 - 0
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 <T> Optional<List<T>> asList(Class<T> t) {
+        return getNativeValue()
+                .map(v -> JSON.parseArray(v, t));
+    }
+
+    protected <T> T convert(String value, Class<T> 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 <T> Optional<T> as(Class<T> 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<String> asString() {
+        return getNativeValue();
+    }
+
+    @Override
+    public Optional<Long> asLong() {
+        return getNativeValue().map(StringUtils::toLong);
+    }
+
+    @Override
+    public Optional<Integer> asInt() {
+        return getNativeValue().map(StringUtils::toInt);
+    }
+
+    @Override
+    public Optional<Double> asDouble() {
+        return getNativeValue().map(StringUtils::toDouble);
+    }
+
+    private Optional<String> getNativeValue() {
+        return Optional.ofNullable(value);
+    }
+
+    @Override
+    public Optional<Object> getValue() {
+        return Optional.ofNullable(value);
+    }
+}

+ 13 - 0
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);
+
+}

+ 26 - 0
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<String> {
+    NONE("无"),
+    R("读"),
+    W("写"),
+    RW("读写");
+    private String text;
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+}

+ 8 - 0
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) {

+ 13 - 0
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();
+}

+ 9 - 0
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);
+}

+ 5 - 2
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();
-
-
 }

+ 4 - 5
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);
 
 }

+ 12 - 0
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);
+}

+ 38 - 0
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<String, Boolean> validator;
+
+    private Supplier<TwoFactorToken> 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();
+    }
+}

+ 47 - 0
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<String, TwoFactorValidatorProvider> 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;
+    }
+}

+ 30 - 0
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));
+    }
+}

+ 70 - 0
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<String, WeakReference<TwoFactorTokenInfo>> 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;
+            }
+        };
+    }
+}

+ 26 - 0
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);
+    }
+}

+ 29 - 0
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());
+    }
+}

+ 1 - 0
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);
     }
+
 }

+ 20 - 4
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> userTokenParser) {

+ 52 - 0
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);
+    }
+}

+ 23 - 0
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")

+ 8 - 0
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<TestModel> {
 
         return ResponseMessage.ok();
     }
+
+    @TwoFactor(value = "test", provider = "test")
+    @GetMapping("/two-factor")
+    public ResponseMessage<String> testTowFactor() {
+        return ResponseMessage.ok();
+    }
 }

+ 39 - 0
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;
+            }
+        };
+    }
+}

+ 56 - 55
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

+ 10 - 0
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方式请求

+ 14 - 1
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<String> {
     private String userId;
 
     @NotBlank(groups = CreateGroup.class)
-
     private String key;
 
     @NotBlank(groups = CreateGroup.class)
@@ -38,4 +38,17 @@ public class UserSettingEntity extends SimpleGenericEntity<String> {
 
     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);
+
+    }
 }

+ 11 - 0
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<UserEntity, String>,
         InsertService<UserEntity, String> {
 
+    /**
+     * 新增用户
+     *
+     * @param data 要添加的数据
+     * @return 用户id
+     * @see org.hswebframework.web.service.authorization.events.UserCreatedEvent
+     * @see BindRoleUserEntity
+     */
+    @Override
+    String insert(UserEntity data);
+
     /**
      * 启用用户
      *

+ 18 - 0
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;
+}

+ 16 - 0
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;
+}

+ 41 - 0
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);
+    }
+}

+ 2 - 0
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<UserEntity, String>
                 trySyncUserRole(userEntity.getId(), bindRoleUserEntity.getRoles());
             }
         }
+        publisher.publishEvent(new UserCreatedEvent(userEntity));
         return userEntity.getId();
     }
 

+ 14 - 0
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<Us
         return userSettingDao;
     }
 
+    @Override
+    protected boolean dataExisted(UserSettingEntity entity) {
+        UserSettingEntity old = createQuery()
+                .where(entity::getUserId)
+                .and(entity::getKey)
+                .and(entity::getSettingId)
+                .single();
+        if (old != null) {
+            entity.setId(old.getId());
+            return true;
+        }
+        return false;
+    }
+
     @Override
     @Cacheable(key = "'user:'+#userId+'.'+#key")
     public List<UserSettingEntity> selectByUser(String userId, String key) {

+ 145 - 0
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.
+ * <p>
+ * 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<Character, Integer> 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);
+        }
+    }
+}

+ 56 - 0
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) + ")");
+        }
+    }
+}

+ 63 - 0
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);
+    }
+
+}

+ 235 - 0
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);
+    }
+}

+ 19 - 3
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);
+    }
 }

+ 9 - 6
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;
 

+ 37 - 0
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)
+    }
+}

+ 3 - 0
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"

+ 25 - 7
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<UserSettingEntity> 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<List<UserSettingEntity>> 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<String> 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);
     }