#103 初步完成双重验证

This commit is contained in:
zhouhao
2018-12-04 18:39:04 +08:00
parent 106006cf10
commit 768033f221
46 changed files with 1459 additions and 94 deletions

View File

@@ -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();
}

View File

@@ -107,10 +107,5 @@ public @interface Authorize {
*/
RequiresDataAccess dataAccess() default @RequiresDataAccess(ignore = true);
/**
* @return 双重验证
*/
TwoFactor twoFactor() default @TwoFactor(ignore = true);
String[] description() default {};
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -31,4 +31,5 @@ public class AopAuthorizeAutoConfiguration {
return new AopAuthorizingController(authorizingHandler, aopMethodAuthorizeDefinitionParser);
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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")

View File

@@ -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();
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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

View File

@@ -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方式请求

View File

@@ -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);
}
}

View File

@@ -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);
/**
* 启用用户
*

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -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);
}