feat:使用自定义表单实现鉴权

This commit is contained in:
wangyu 2022-10-01 15:17:00 +08:00
parent c62cdff45d
commit 8fbc8ad6b1
8 changed files with 207 additions and 90 deletions

View File

@ -28,5 +28,9 @@
<artifactId>flyfish-web</artifactId> <artifactId>flyfish-web</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.dingxiang-inc</groupId>
<artifactId>ctu-client-sdk</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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<Authentication> 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<UserDetails> retrieveUser(String username) {
return this.userDetailsService.findByUsername(username);
}
}

View File

@ -1,5 +1,7 @@
package com.flyfish.framework.config; 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.config.properties.SecurityProperties;
import com.flyfish.framework.configuration.jwt.JwtProperties; import com.flyfish.framework.configuration.jwt.JwtProperties;
import com.flyfish.framework.configuration.jwt.JwtSecurityContextRepository; 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.AuthenticationAuditor;
import com.flyfish.framework.service.AuthenticationLogger; import com.flyfish.framework.service.AuthenticationLogger;
import com.flyfish.framework.service.UserService; 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.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.ReactiveAuthenticationManager; 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.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService; 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.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain; 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.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -120,8 +125,10 @@ public class WebSecurityConfig {
TokenProvider tokenProvider, TokenProvider tokenProvider,
SecurityProperties properties, SecurityProperties properties,
ReactiveUserDetailsService userDetailsService, ReactiveUserDetailsService userDetailsService,
ServerAuthenticationConverter authenticationConverter,
AuthenticationAuditor authenticationAuditor) { AuthenticationAuditor authenticationAuditor) {
http ReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
return http
.securityContextRepository(contextRepository()) .securityContextRepository(contextRepository())
.authorizeExchange() .authorizeExchange()
.pathMatchers(Stream.concat(Stream.of(properties.getAllowUris()), Stream.of("/api/logout", "/api/login")) .pathMatchers(Stream.concat(Stream.of(properties.getAllowUris()), Stream.of("/api/logout", "/api/login"))
@ -130,31 +137,24 @@ public class WebSecurityConfig {
.anyExchange().authenticated() .anyExchange().authenticated()
.and() .and()
.formLogin() // 配置登录节点 .formLogin() // 配置登录节点
.authenticationManager(authenticationManager(properties, userDetailsService)) .disable()
.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)) .httpBasic()
.authenticationSuccessHandler(new JsonAuthenticationSuccessHandler(authenticationAuditor)) .disable()
.authenticationFailureHandler(new JsonAuthenticationFailureHandler(authenticationAuditor))
.requiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login", "/api/login"))
.and()
.logout() .logout()
.logoutUrl("/api/logout") .logoutUrl("/api/logout")
.logoutSuccessHandler(new JsonLogoutSuccessHandler(authenticationAuditor, tokenProvider)) .logoutSuccessHandler(new JsonLogoutSuccessHandler(authenticationAuditor, tokenProvider))
.and() .and()
.csrf().disable(); .csrf().disable()
return http.build(); .addFilterAt(
configure(properties, authenticationManager, authenticationAuditor, authenticationConverter),
SecurityWebFiltersOrder.FORM_LOGIN)
.build();
} }
/** @Bean
* 构建鉴权管理器 public ServerAuthenticationConverter encryptedAuthenticateConverter(SecurityProperties securityProperties,
* ObjectProvider<DxCaptchaValidator> validator) {
* @param userDetailsService 用户详情服务 return new EncryptedAuthenticationConverter(securityProperties, validator);
* @return 结果
*/
public ReactiveAuthenticationManager authenticationManager(SecurityProperties securityProperties,
ReactiveUserDetailsService userDetailsService) {
RSAAuthenticationManager authenticationManager = new RSAAuthenticationManager(securityProperties, userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
} }
/** /**
@ -188,4 +188,37 @@ public class WebSecurityConfig {
}).subscribe(); }).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);
}
} }

View File

@ -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("验证码验证异常!");
}
}
}

View File

@ -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<DxCaptchaValidator> validator;
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return exchange.getFormData().map(this::createAuthentication);
}
private UsernamePasswordAuthenticationToken createAuthentication(MultiValueMap<String, String> 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());
}
}
}

View File

@ -17,4 +17,27 @@ public class SecurityProperties {
// 启用rsa // 启用rsa
private boolean 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;
}
} }

View File

@ -103,6 +103,11 @@
<artifactId>jasypt-spring-boot-starter</artifactId> <artifactId>jasypt-spring-boot-starter</artifactId>
<version>${jasypt.version}</version> <version>${jasypt.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.dingxiang-inc</groupId>
<artifactId>ctu-client-sdk</artifactId>
<version>2.4</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>