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 +