feat:使用自定义表单实现鉴权
This commit is contained in:
parent
c62cdff45d
commit
8fbc8ad6b1
@ -28,5 +28,9 @@
|
||||
<artifactId>flyfish-web</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.dingxiang-inc</groupId>
|
||||
<artifactId>ctu-client-sdk</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<DxCaptchaValidator> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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("验证码验证异常!");
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
5
pom.xml
5
pom.xml
@ -103,6 +103,11 @@
|
||||
<artifactId>jasypt-spring-boot-starter</artifactId>
|
||||
<version>${jasypt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.dingxiang-inc</groupId>
|
||||
<artifactId>ctu-client-sdk</artifactId>
|
||||
<version>2.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user