feat:使用自定义表单实现鉴权
This commit is contained in:
parent
c62cdff45d
commit
8fbc8ad6b1
@ -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>
|
||||||
|
@ -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;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
// 启用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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
5
pom.xml
5
pom.xml
@ -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>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user