refactor(security): 修复会话并发限制不生效问题
This commit is contained in:
parent
f0066d4c64
commit
d1834b404b
@ -8,12 +8,14 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author harry_yao
|
||||
@ -27,7 +29,7 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException ex) throws IOException, ServletException {
|
||||
ex.printStackTrace();
|
||||
// ex.printStackTrace();
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
ExceptionResult result;
|
||||
if (ex instanceof MissingCsrfTokenException) {
|
||||
@ -36,6 +38,11 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
result = new ExceptionResult("凭证已过期,请重新登录", HttpStatus.UNAUTHORIZED.value(),
|
||||
LocalDateTime.now());
|
||||
} else if (ex instanceof AuthorizationDeniedException) {
|
||||
// 403
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
result = new ExceptionResult("当前账号已在其他设备登录,请先退出再尝试登录", HttpStatus.FORBIDDEN.value(),
|
||||
LocalDateTime.now());
|
||||
} else {
|
||||
// 403
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
|
@ -24,7 +24,7 @@ public class CustomSessionInformationExpiredStrategy implements SessionInformati
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
response.getWriter().print(objectMapper.writeValueAsString(Map.of(
|
||||
"msg", "会话已过期(有可能是您同时登录了太多的太多的客户端)",
|
||||
"msg", "会话已过期(有可能是您同时登录了太多的客户端)",
|
||||
"code", HttpStatus.UNAUTHORIZED.value(),
|
||||
"timestamp", LocalDateTime.now()
|
||||
)));
|
||||
|
@ -1,30 +0,0 @@
|
||||
package com.zsc.edu.dify.framework.security;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||
|
||||
/**
|
||||
* @author harry_yao
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityBeanConfig {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionRegistry sessionRegistry() {
|
||||
return new SessionRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
||||
return new HttpSessionEventPublisher();
|
||||
}
|
||||
}
|
@ -12,11 +12,15 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||
|
||||
@ -35,31 +39,51 @@ public class SpringSecurityConfig {
|
||||
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
|
||||
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
|
||||
private final CustomAccessDeniedHandler customAccessDeniedHandler;
|
||||
private final SessionRegistry sessionRegistry;
|
||||
private final SecurityBeanConfig securityBeanConfig;
|
||||
// private final SessionRegistry sessionRegistry;
|
||||
private final CustomSessionInformationExpiredStrategy customSessionInformationExpiredStrategy;
|
||||
|
||||
@Resource
|
||||
private final DataSource dataSource;
|
||||
|
||||
// @Bean
|
||||
// public BCryptPasswordEncoder bCryptPasswordEncoder() {
|
||||
// return new BCryptPasswordEncoder();
|
||||
// };
|
||||
@Bean
|
||||
public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
|
||||
return new HttpSessionSecurityContextRepository();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy() {
|
||||
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
|
||||
concurrentSessionControlAuthenticationStrategy.setMaximumSessions(1);
|
||||
return concurrentSessionControlAuthenticationStrategy;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionRegistry sessionRegistry() {
|
||||
return new SessionRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
||||
return new HttpSessionEventPublisher();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
|
||||
tokenRepository.setDataSource(dataSource);
|
||||
return tokenRepository;
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthenticationManager authenticationManager() {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
||||
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
||||
daoAuthenticationProvider.setPasswordEncoder(securityBeanConfig.passwordEncoder());
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||
return new ProviderManager(daoAuthenticationProvider);
|
||||
}
|
||||
|
||||
@ -71,7 +95,8 @@ public class SpringSecurityConfig {
|
||||
filter.setFilterProcessesUrl("/api/rest/user/login");
|
||||
filter.setAuthenticationManager(authenticationManager());
|
||||
// 将登录后的请求信息保存到Session中,不然会报null
|
||||
filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
|
||||
// filter.setSessionAuthenticationStrategy(concurrentSessionControlAuthenticationStrategy());
|
||||
// filter.setSecurityContextRepository(httpSessionSecurityContextRepository());
|
||||
return filter;
|
||||
}
|
||||
|
||||
@ -86,43 +111,6 @@ public class SpringSecurityConfig {
|
||||
.requestMatchers("/v1/**").authenticated()
|
||||
.requestMatchers("/api/**").authenticated()
|
||||
)
|
||||
// 不用注解,直接通过判断路径实现动态访问权限
|
||||
// .requestMatchers("/api/**").access((authentication, object) -> {
|
||||
// //表示请求的 URL 地址和数据库的地址是否匹配上了
|
||||
// boolean isMatch = false;
|
||||
// //获取当前请求的 URL 地址
|
||||
// String requestURI = object.getRequest().getRequestURI();
|
||||
// List<MenuWithRoleVO> menuWithRole = menuService.getMenuWithRole();
|
||||
// for (MenuWithRoleVO m : menuWithRole) {
|
||||
// AntPathMatcher antPathMatcher = new AntPathMatcher();
|
||||
// if (antPathMatcher.match(m.getUrl(), requestURI)) {
|
||||
// isMatch = true;
|
||||
// //说明找到了请求的地址了
|
||||
// //这就是当前请求需要的角色
|
||||
// List<Role> roles = m.getRoles();
|
||||
// //获取当前登录用户的角色
|
||||
// Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
|
||||
// for (GrantedAuthority authority : authorities) {
|
||||
// for (Role role : roles) {
|
||||
// if (authority.getAuthority().equals(role.getName())) {
|
||||
// //说明当前登录用户具备当前请求所需要的角色
|
||||
// return new AuthorizationDecision(true);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (!isMatch) {
|
||||
// //说明请求的 URL 地址和数据库的地址没有匹配上,对于这种请求,统一只要登录就能访问
|
||||
// if (authentication.get() instanceof AnonymousAuthenticationToken) {
|
||||
// return new AuthorizationDecision(false);
|
||||
// } else {
|
||||
// //说明用户已经认证了
|
||||
// return new AuthorizationDecision(true);
|
||||
// }
|
||||
// }
|
||||
// return new AuthorizationDecision(false);
|
||||
// }))
|
||||
.addFilterAt(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/user/login")
|
||||
@ -140,11 +128,13 @@ public class SpringSecurityConfig {
|
||||
.rememberMe(rememberMe -> rememberMe
|
||||
.userDetailsService(userDetailsService)
|
||||
.tokenRepository(persistentTokenRepository()))
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers("v1/**","/api/internal/**", "/api/rest/user/logout","/api/rest/user/register"))
|
||||
.csrf(csrf ->
|
||||
csrf.ignoringRequestMatchers("v1/**","/api/internal/**", "/api/rest/user/logout","/api/rest/user/register"))
|
||||
.sessionManagement(session -> session
|
||||
.maximumSessions(1)
|
||||
.sessionRegistry(sessionRegistry)
|
||||
.expiredSessionStrategy(customSessionInformationExpiredStrategy))
|
||||
.build();
|
||||
.maxSessionsPreventsLogin(true)
|
||||
.sessionRegistry(sessionRegistry())
|
||||
// .expiredSessionStrategy(customSessionInformationExpiredStrategy)
|
||||
).build();
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -98,4 +99,34 @@ public class UserDetailsImpl implements UserDetails {
|
||||
return enableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
UserDetailsImpl that = (UserDetailsImpl) o;
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, username, password, enableState, name, dept, role, roles, authorities, permissions, dataScopeDeptIds, deptId, createId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserDetailsImpl{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
", password='" + password + '\'' +
|
||||
", enableState=" + enableState +
|
||||
", name='" + name + '\'' +
|
||||
", dept=" + dept +
|
||||
", role=" + role +
|
||||
", roles=" + roles +
|
||||
", authorities=" + authorities +
|
||||
", permissions=" + permissions +
|
||||
", dataScopeDeptIds=" + dataScopeDeptIds +
|
||||
", deptId=" + deptId +
|
||||
", createId=" + createId +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user