refactor(security): 修复会话并发限制不生效问题
This commit is contained in:
parent
f0066d4c64
commit
d1834b404b
@ -8,12 +8,14 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author harry_yao
|
* @author harry_yao
|
||||||
@ -27,7 +29,7 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||||
AccessDeniedException ex) throws IOException, ServletException {
|
AccessDeniedException ex) throws IOException, ServletException {
|
||||||
ex.printStackTrace();
|
// ex.printStackTrace();
|
||||||
response.setContentType("application/json;charset=utf-8");
|
response.setContentType("application/json;charset=utf-8");
|
||||||
ExceptionResult result;
|
ExceptionResult result;
|
||||||
if (ex instanceof MissingCsrfTokenException) {
|
if (ex instanceof MissingCsrfTokenException) {
|
||||||
@ -36,6 +38,11 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
|||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
result = new ExceptionResult("凭证已过期,请重新登录", HttpStatus.UNAUTHORIZED.value(),
|
result = new ExceptionResult("凭证已过期,请重新登录", HttpStatus.UNAUTHORIZED.value(),
|
||||||
LocalDateTime.now());
|
LocalDateTime.now());
|
||||||
|
} else if (ex instanceof AuthorizationDeniedException) {
|
||||||
|
// 403
|
||||||
|
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||||
|
result = new ExceptionResult("当前账号已在其他设备登录,请先退出再尝试登录", HttpStatus.FORBIDDEN.value(),
|
||||||
|
LocalDateTime.now());
|
||||||
} else {
|
} else {
|
||||||
// 403
|
// 403
|
||||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||||
|
@ -24,7 +24,7 @@ public class CustomSessionInformationExpiredStrategy implements SessionInformati
|
|||||||
response.setContentType("application/json;charset=utf-8");
|
response.setContentType("application/json;charset=utf-8");
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
response.getWriter().print(objectMapper.writeValueAsString(Map.of(
|
response.getWriter().print(objectMapper.writeValueAsString(Map.of(
|
||||||
"msg", "会话已过期(有可能是您同时登录了太多的太多的客户端)",
|
"msg", "会话已过期(有可能是您同时登录了太多的客户端)",
|
||||||
"code", HttpStatus.UNAUTHORIZED.value(),
|
"code", HttpStatus.UNAUTHORIZED.value(),
|
||||||
"timestamp", LocalDateTime.now()
|
"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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
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.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.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
|
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
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.context.HttpSessionSecurityContextRepository;
|
||||||
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||||
|
|
||||||
@ -35,31 +39,51 @@ public class SpringSecurityConfig {
|
|||||||
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
|
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
|
||||||
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
|
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
|
||||||
private final CustomAccessDeniedHandler customAccessDeniedHandler;
|
private final CustomAccessDeniedHandler customAccessDeniedHandler;
|
||||||
private final SessionRegistry sessionRegistry;
|
// private final SessionRegistry sessionRegistry;
|
||||||
private final SecurityBeanConfig securityBeanConfig;
|
|
||||||
private final CustomSessionInformationExpiredStrategy customSessionInformationExpiredStrategy;
|
private final CustomSessionInformationExpiredStrategy customSessionInformationExpiredStrategy;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
|
|
||||||
// @Bean
|
@Bean
|
||||||
// public BCryptPasswordEncoder bCryptPasswordEncoder() {
|
public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
|
||||||
// return new BCryptPasswordEncoder();
|
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
|
@Bean
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
|
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
|
||||||
tokenRepository.setDataSource(dataSource);
|
tokenRepository.setDataSource(dataSource);
|
||||||
return tokenRepository;
|
return tokenRepository;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
AuthenticationManager authenticationManager() {
|
AuthenticationManager authenticationManager() {
|
||||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
||||||
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
||||||
daoAuthenticationProvider.setPasswordEncoder(securityBeanConfig.passwordEncoder());
|
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||||
return new ProviderManager(daoAuthenticationProvider);
|
return new ProviderManager(daoAuthenticationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +95,8 @@ public class SpringSecurityConfig {
|
|||||||
filter.setFilterProcessesUrl("/api/rest/user/login");
|
filter.setFilterProcessesUrl("/api/rest/user/login");
|
||||||
filter.setAuthenticationManager(authenticationManager());
|
filter.setAuthenticationManager(authenticationManager());
|
||||||
// 将登录后的请求信息保存到Session中,不然会报null
|
// 将登录后的请求信息保存到Session中,不然会报null
|
||||||
filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
|
// filter.setSessionAuthenticationStrategy(concurrentSessionControlAuthenticationStrategy());
|
||||||
|
// filter.setSecurityContextRepository(httpSessionSecurityContextRepository());
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,43 +111,6 @@ public class SpringSecurityConfig {
|
|||||||
.requestMatchers("/v1/**").authenticated()
|
.requestMatchers("/v1/**").authenticated()
|
||||||
.requestMatchers("/api/**").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)
|
.addFilterAt(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||||
.formLogin(form -> form
|
.formLogin(form -> form
|
||||||
.loginPage("/user/login")
|
.loginPage("/user/login")
|
||||||
@ -140,11 +128,13 @@ public class SpringSecurityConfig {
|
|||||||
.rememberMe(rememberMe -> rememberMe
|
.rememberMe(rememberMe -> rememberMe
|
||||||
.userDetailsService(userDetailsService)
|
.userDetailsService(userDetailsService)
|
||||||
.tokenRepository(persistentTokenRepository()))
|
.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
|
.sessionManagement(session -> session
|
||||||
.maximumSessions(1)
|
.maximumSessions(1)
|
||||||
.sessionRegistry(sessionRegistry)
|
.maxSessionsPreventsLogin(true)
|
||||||
.expiredSessionStrategy(customSessionInformationExpiredStrategy))
|
.sessionRegistry(sessionRegistry())
|
||||||
.build();
|
// .expiredSessionStrategy(customSessionInformationExpiredStrategy)
|
||||||
|
).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -98,4 +99,34 @@ public class UserDetailsImpl implements UserDetails {
|
|||||||
return enableState;
|
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