From 8fbc8ad6b166be9c7bff21fafa3218eec0439982 Mon Sep 17 00:00:00 2001
From: wangyu <727842003@qq.com>
Date: Sat, 1 Oct 2022 15:17:00 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E4=BD=BF=E7=94=A8=E8=87=AA?=
=?UTF-8?q?=E5=AE=9A=E4=B9=89=E8=A1=A8=E5=8D=95=E5=AE=9E=E7=8E=B0=E9=89=B4?=
=?UTF-8?q?=E6=9D=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
flyfish-user/pom.xml | 4 +
.../config/RSAAuthenticationManager.java | 68 ----------------
.../framework/config/WebSecurityConfig.java | 77 +++++++++++++------
.../config/captcha/DxCaptchaValidator.java | 47 +++++++++++
.../EncryptedAuthenticationConverter.java | 73 ++++++++++++++++++
.../config/properties/SecurityProperties.java | 23 ++++++
.../framework/configuration/mail/.gitkeep | 0
pom.xml | 5 ++
8 files changed, 207 insertions(+), 90 deletions(-)
delete mode 100644 flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java
create mode 100644 flyfish-user/src/main/java/com/flyfish/framework/config/captcha/DxCaptchaValidator.java
create mode 100644 flyfish-user/src/main/java/com/flyfish/framework/config/converter/EncryptedAuthenticationConverter.java
create mode 100644 flyfish-web/src/main/java/com/flyfish/framework/configuration/mail/.gitkeep
diff --git a/flyfish-user/pom.xml b/flyfish-user/pom.xml
index dcb582f..9b50bf8 100644
--- a/flyfish-user/pom.xml
+++ b/flyfish-user/pom.xml
@@ -28,5 +28,9 @@
flyfish-web
${project.version}
+
+ com.dingxiang-inc
+ ctu-client-sdk
+
diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java b/flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java
deleted file mode 100644
index 761ac25..0000000
--- a/flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.flyfish.framework.config;
-
-import com.flyfish.framework.config.properties.SecurityProperties;
-import com.flyfish.framework.utils.RSAUtils;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.util.Assert;
-import reactor.core.publisher.Mono;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-/**
- * 支持rsa的认证管理器
- *
- * @author wangyu
- */
-@Slf4j
-public class RSAAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
-
- private final ReactiveUserDetailsService userDetailsService;
-
- private final Boolean rsa;
-
- public RSAAuthenticationManager(SecurityProperties securityProperties, ReactiveUserDetailsService userDetailsService) {
- Assert.notNull(userDetailsService, "userDetailsService cannot be null");
- this.rsa = securityProperties.isRsa();
- this.userDetailsService = userDetailsService;
- }
-
- @Override
- public Mono authenticate(Authentication authentication) {
- if (rsa && !authentication.isAuthenticated()) {
- Object credentials = authentication.getCredentials();
- if (credentials instanceof String) {
- Authentication mapped = createAuthentication(authentication);
- return super.authenticate(mapped);
- }
- }
- return super.authenticate(authentication);
- }
-
- private UsernamePasswordAuthenticationToken createAuthentication(Authentication authentication) throws IllegalArgumentException {
- String password = (String) authentication.getCredentials();
- try {
- password = RSAUtils.decrypt(password, RSAKeys.PRIVATE_KEY);
- } catch (IllegalBlockSizeException | InvalidKeyException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException e) {
- log.error("尝试解密密码出错", e);
- throw new IllegalArgumentException("非法请求!密码格式校验失败!");
- } catch (IllegalArgumentException e) {
- log.error("抛出参数异常", e);
- throw new IllegalArgumentException("密码未加密,请求无效!" + e.getMessage());
- }
- return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), password, authentication.getAuthorities());
- }
-
- @Override
- protected Mono retrieveUser(String username) {
- return this.userDetailsService.findByUsername(username);
- }
-}
diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java b/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java
index 256334a..181b826 100644
--- a/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java
+++ b/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java
@@ -1,5 +1,7 @@
package com.flyfish.framework.config;
+import com.flyfish.framework.config.captcha.DxCaptchaValidator;
+import com.flyfish.framework.config.converter.EncryptedAuthenticationConverter;
import com.flyfish.framework.config.properties.SecurityProperties;
import com.flyfish.framework.configuration.jwt.JwtProperties;
import com.flyfish.framework.configuration.jwt.JwtSecurityContextRepository;
@@ -15,15 +17,17 @@ import com.flyfish.framework.initializer.UserInitializer;
import com.flyfish.framework.service.AuthenticationAuditor;
import com.flyfish.framework.service.AuthenticationLogger;
import com.flyfish.framework.service.UserService;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
@@ -33,7 +37,8 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
-import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
+import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import reactor.core.publisher.Mono;
@@ -120,8 +125,10 @@ public class WebSecurityConfig {
TokenProvider tokenProvider,
SecurityProperties properties,
ReactiveUserDetailsService userDetailsService,
+ ServerAuthenticationConverter authenticationConverter,
AuthenticationAuditor authenticationAuditor) {
- http
+ ReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
+ return http
.securityContextRepository(contextRepository())
.authorizeExchange()
.pathMatchers(Stream.concat(Stream.of(properties.getAllowUris()), Stream.of("/api/logout", "/api/login"))
@@ -130,31 +137,24 @@ public class WebSecurityConfig {
.anyExchange().authenticated()
.and()
.formLogin() // 配置登录节点
- .authenticationManager(authenticationManager(properties, userDetailsService))
- .authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
- .authenticationSuccessHandler(new JsonAuthenticationSuccessHandler(authenticationAuditor))
- .authenticationFailureHandler(new JsonAuthenticationFailureHandler(authenticationAuditor))
- .requiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login", "/api/login"))
- .and()
+ .disable()
+ .httpBasic()
+ .disable()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(new JsonLogoutSuccessHandler(authenticationAuditor, tokenProvider))
.and()
- .csrf().disable();
- return http.build();
+ .csrf().disable()
+ .addFilterAt(
+ configure(properties, authenticationManager, authenticationAuditor, authenticationConverter),
+ SecurityWebFiltersOrder.FORM_LOGIN)
+ .build();
}
- /**
- * 构建鉴权管理器
- *
- * @param userDetailsService 用户详情服务
- * @return 结果
- */
- public ReactiveAuthenticationManager authenticationManager(SecurityProperties securityProperties,
- ReactiveUserDetailsService userDetailsService) {
- RSAAuthenticationManager authenticationManager = new RSAAuthenticationManager(securityProperties, userDetailsService);
- authenticationManager.setPasswordEncoder(passwordEncoder());
- return authenticationManager;
+ @Bean
+ public ServerAuthenticationConverter encryptedAuthenticateConverter(SecurityProperties securityProperties,
+ ObjectProvider validator) {
+ return new EncryptedAuthenticationConverter(securityProperties, validator);
}
/**
@@ -188,4 +188,37 @@ public class WebSecurityConfig {
}).subscribe();
};
}
+
+ /**
+ * 配置登录相关参数
+ *
+ * @param properties 安全属性
+ * @param authenticationAuditor 审计器
+ * @param authenticationConverter 转换器
+ * @param authenticationManager 鉴权管理器
+ * @return 结果
+ */
+ private AuthenticationWebFilter configure(SecurityProperties properties,
+ ReactiveAuthenticationManager authenticationManager,
+ AuthenticationAuditor authenticationAuditor,
+ ServerAuthenticationConverter authenticationConverter) {
+ AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(authenticationManager);
+ authenticationFilter.setRequiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login", "/api/login"));
+ authenticationFilter.setAuthenticationFailureHandler(new JsonAuthenticationFailureHandler(authenticationAuditor));
+ authenticationFilter.setServerAuthenticationConverter(authenticationConverter);
+ authenticationFilter.setAuthenticationSuccessHandler(new JsonAuthenticationSuccessHandler(authenticationAuditor));
+ authenticationFilter.setSecurityContextRepository(contextRepository());
+ return authenticationFilter;
+ }
+
+ /**
+ * 按需启用验证器
+ *
+ * @return 结果
+ */
+ @Bean
+ @ConditionalOnProperty(value = "security.captcha.enable", havingValue = "true")
+ public DxCaptchaValidator dxCaptchaValidator(SecurityProperties properties) {
+ return new DxCaptchaValidator(properties);
+ }
}
diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/captcha/DxCaptchaValidator.java b/flyfish-user/src/main/java/com/flyfish/framework/config/captcha/DxCaptchaValidator.java
new file mode 100644
index 0000000..0babd6c
--- /dev/null
+++ b/flyfish-user/src/main/java/com/flyfish/framework/config/captcha/DxCaptchaValidator.java
@@ -0,0 +1,47 @@
+package com.flyfish.framework.config.captcha;
+
+import com.dingxianginc.ctu.client.CaptchaClient;
+import com.dingxianginc.ctu.client.model.CaptchaResponse;
+import com.flyfish.framework.config.properties.SecurityProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.BooleanUtils;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.stereotype.Component;
+
+/**
+ * 顶象验证码验证器
+ *
+ * @author wangyu
+ */
+@Slf4j
+public class DxCaptchaValidator {
+
+ private final CaptchaClient captchaClient;
+
+ public DxCaptchaValidator(SecurityProperties securityProperties) {
+ SecurityProperties.CaptchaProperties captchaProperties = securityProperties.getCaptcha();
+ this.captchaClient = new CaptchaClient(captchaProperties.getAppId(), captchaProperties.getAppSecret());
+ captchaClient.setCaptchaUrl("https://" + captchaProperties.getServer() + "/api/tokenVerify");
+ }
+
+ /**
+ * 执行验证码验证
+ *
+ * @param token 验证码
+ * @return 结果
+ */
+ public void verify(String token) {
+ //指定服务器地址,saas可在控制台,应用管理页面最上方获取
+ try {
+ CaptchaResponse response = captchaClient.verifyToken(token);
+ log.info(response.getCaptchaStatus());
+ if (BooleanUtils.isNotTrue(response.getResult())) {
+ throw new BadCredentialsException("验证码不正确!");
+ }
+ } catch (Exception e) {
+ log.error("验证码验证失败!" + e.getMessage());
+ throw new BadCredentialsException("验证码验证异常!");
+ }
+ }
+}
diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/converter/EncryptedAuthenticationConverter.java b/flyfish-user/src/main/java/com/flyfish/framework/config/converter/EncryptedAuthenticationConverter.java
new file mode 100644
index 0000000..0eaa082
--- /dev/null
+++ b/flyfish-user/src/main/java/com/flyfish/framework/config/converter/EncryptedAuthenticationConverter.java
@@ -0,0 +1,73 @@
+package com.flyfish.framework.config.converter;
+
+import com.flyfish.framework.config.RSAKeys;
+import com.flyfish.framework.config.captcha.DxCaptchaValidator;
+import com.flyfish.framework.config.properties.SecurityProperties;
+import com.flyfish.framework.utils.RSAUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 超级简单的认证转换器
+ *
+ * @author wangyu
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class EncryptedAuthenticationConverter implements ServerAuthenticationConverter {
+
+ private final String usernameParameter = "username";
+
+ private final String passwordParameter = "password";
+
+ private final String tokenParameter = "token";
+
+ private final SecurityProperties securityProperties;
+
+ private final ObjectProvider validator;
+
+ @Override
+ public Mono convert(ServerWebExchange exchange) {
+ return exchange.getFormData().map(this::createAuthentication);
+ }
+
+ private UsernamePasswordAuthenticationToken createAuthentication(MultiValueMap data) {
+ String username = data.getFirst(this.usernameParameter);
+ String password = data.getFirst(this.passwordParameter);
+ String token = data.getFirst(this.tokenParameter);
+ validator.ifAvailable(v -> v.verify(token));
+ if (securityProperties.isRsa()) {
+ password = decrypt(password);
+ }
+ val at = new UsernamePasswordAuthenticationToken(username, password);
+ at.setDetails(token);
+ return at;
+ }
+
+ private String decrypt(String password) {
+ try {
+ return RSAUtils.decrypt(password, RSAKeys.PRIVATE_KEY);
+ } catch (IllegalBlockSizeException | InvalidKeyException | BadPaddingException | NoSuchAlgorithmException |
+ NoSuchPaddingException e) {
+ log.error("尝试解密密码出错", e);
+ throw new IllegalArgumentException("非法请求!密码格式校验失败!");
+ } catch (IllegalArgumentException e) {
+ log.error("抛出参数异常", e);
+ throw new IllegalArgumentException("密码未加密,请求无效!" + e.getMessage());
+ }
+ }
+}
diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java b/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java
index f3d3df6..82e555b 100644
--- a/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java
+++ b/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java
@@ -17,4 +17,27 @@ public class SecurityProperties {
// 启用rsa
private boolean rsa;
+
+ // 验证码配置
+ private CaptchaProperties captcha = new CaptchaProperties();
+
+ /**
+ * 验证码属性
+ * @author wangyu
+ */
+ @Data
+ public static class CaptchaProperties {
+
+ // 启用验证码
+ private boolean enable;
+
+ // 顶象appid
+ private String appId;
+
+ // 顶象appSecret
+ private String appSecret;
+
+ // 服务器地址
+ private String server;
+ }
}
diff --git a/flyfish-web/src/main/java/com/flyfish/framework/configuration/mail/.gitkeep b/flyfish-web/src/main/java/com/flyfish/framework/configuration/mail/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/pom.xml b/pom.xml
index bc9cea4..8fa078c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,11 @@
jasypt-spring-boot-starter
${jasypt.version}
+
+ com.dingxiang-inc
+ ctu-client-sdk
+ 2.4
+