关于使用SpringSecurity不能设置Session并发无效、剔除前一个用户无效的核心解决方案

本文介绍如何在SpringBoot项目中使用SpringSecurity控制用户会话并发数,特别是针对单个用户登录时如何正确地踢出之前的登录会话。文章详细解释了通过重写自定义User对象的equals、toString及hashCode方法实现这一目标的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

那些年掉过SpringSecurity的坑。

最近在研究SpringBoot集成SpringSecurity权限框架的Demo,关于怎么集成,网上一大片的文章,我就不废话多说了,自行百度解决。


我使用的是注解配置,类继承WebSecurityConfigurerAdapter适配器,重写configure(HttpSecurity http)方法配置参数等。


然后是继承UserDetails抽象接口实现自定义的User对象,而SpringSecurity管理Session也是通过管理User对象实现的。


然后到了配置Session的并发步骤了,不管是通过HttpSecurity配置还是XML方式配置其目的都是一样的,以下是HttpSecurity配置。

http.sessionManagement()//Session管理器
	.maximumSessions(1)
	.sessionRegistry(sessionRegistry)
	.expiredUrl("/Member/Login.html");
好了,maximumSessions(1)是配置好了,然后去启动项目,然后你会发现登录同一个账号,并没有剔除前一个账号,这是怎么回事呢?

抛开框架暂且不说,如果是我直接写一个剔除前一个用户的话,我肯定是要得到全部用户,然后挨个挨个遍历筛选,如果存在当前登录用户就剔除前一个用户,这个需要使用if比较来实现的。

好了,问题找到了,因为SpringSecurity是通过管理UserDetails对象来实现用户管理的,按照上一步的原理,是需要进行比较实现效果的,然后呢,类的比较是不能用==比较的,类之间的比较是通过类的equals方法进行比较的,好了,问题找到了,我们自定义的User对象是没有实现equals方法的,好了,我们现在开始重写equals方法吧,关于重写的规则是:如果要重写equals方法,那么就必须要重写toStirng方法,如果要重写toString方法就最好要重写hashCode方法,所以我们需要在自定义的User对象中重写三个方法,hashCode、toString和equals方法。

	@Override
	public String toString() {
		return this.userName;
	}
	
	@Override
	public int hashCode() {
		return userName.hashCode();
	}
	
	@Override
	public boolean equals(Object obj) {
		return this.toString().equals(obj.toString());
	}


下面是我自定义的User类(只是示例,没有配置权限相关):
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.hong610.domain.User;

/**
 * SpringSecurity User
 * @author Hong
 * @Date 2016年11月20日
 */
public class UserDetail implements UserDetails {
	
	private String userName;

	private String userPwd;

	private static final long serialVersionUID = 1L;

	public UserDetail(User user) {
		this.setUserName(user.getUserName());
		this.setUserPwd(user.getUserPwd());
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return null;
	}

	@Override
	public String getPassword() {
		return this.getUserPwd();
	}

	@Override
	public String getUsername() {
		return this.getUserName();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getUserPwd() {
		return userPwd;
	}

	public void setUserPwd(String userPwd) {
		this.userPwd = userPwd;
	}
	
	@Override
	public String toString() {
		return this.userName;
	}
	
	@Override
	public int hashCode() {
		return userName.hashCode();
	}
	
	@Override
	public boolean equals(Object obj) {
		return this.toString().equals(obj.toString());
	}
}

Okay,重启项目,剔除前一个用户成功!

关于重写的规则参照《Effective Java》书籍。


<think>好的,我现在需要帮助用户将他们的lowcodeTokenFilter整合到响应式网关中。用户提供的代码是基于传统的Spring Web Security配置,使用的是WebSecurityConfigurerAdapter,这在响应式环境中不再适用。首先,我需要确认响应式网关使用的是Spring Cloud Gateway,并且是基于Spring WebFlux的,所以安全配置应该使用Reactive版的Spring Security。 接下来,我得回忆一下Spring Security的响应式配置方式。传统的是使用WebSecurityConfigurerAdapter,而响应式则需要通过@EnableWebFluxSecurity注解,并配置SecurityWebFilterChain的Bean。用户原来的CustomSecurityConfig继承自WebSecurityConfigurerAdapter,现在需要改为定义SecurityWebFilterChain。 然后,用户提到的lowcodeTokenFilter是一个Servlet过滤器,这在响应式环境中不适用。因为响应式使用的是WebFilter,所以需要将lowcodeTokenFilter转换为WebFilter或者自定义的ServerWebExchange修改逻辑。可能需要检查lowcodeTokenFilter的具体功能,比如是否处理认证令牌,将其转换为响应式的实现。 另外,用户原来的配置中使用了addFilterBefore,将lowcodeTokenFilter添加到UsernamePasswordAuthenticationFilter之。在响应式环境中,需要将自定义的WebFilter添加到过滤器链中的适当位置。可能需要使用SecurityWebFiltersOrder中的常量来确定顺序,比如在SecurityWebFiltersOrder.AUTHENTICATION之添加。 然后是认证提供者的问题。原来的lowcodeTokenAuthenticationProvider是一个AuthenticationProvider,这在响应式环境中应该替换为ReactiveAuthenticationManager。需要创建一个自定义的ReactiveAuthenticationManager,并利用lowcodeTokenAuthenticationProvider的逻辑,可能需要进行适配,将阻塞的代码转换为非阻塞的。 用户的配置中还禁用了CSRF,设置了无状态会话,这些在响应式配置中同样需要处理。可以通过调用.csrf().disable()和.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)来实现。 最后,授权配置需要调整,原来的antMatchers在响应式中使用pathMatchers,并且通过.access()方法结合AuthorizationManager来处理权限。比如允许匿名访问某些路径,其他路径需要认证。 总结步骤应该是:转换过滤器为WebFilter,配置SecurityWebFilterChain,调整认证管理,设置安全策略,处理授权。同时需要注意响应式编程的非阻塞特性,避免使用阻塞代码。可能还需要处理全局安全上下文和异常处理的问题,比如使用自定义的ServerAuthenticationEntryPoint来处理未认证的请求。</think>将传统Spring Security过滤器迁移到响应式网关(如Spring Cloud Gateway)需要重构安全配置架构。以下是分步改造方案: 1. **基础配置转换** ```java @Configuration @EnableWebFluxSecurity // 替换@EnableWebSecurity @Order(101) public class CustomSecurityConfig { @Resource private LowcodeTokenWebFilter lowcodeTokenWebFilter; // 需转换为响应式WebFilter @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http .csrf().disable() .exceptionHandling() .and() .addFilterAt(lowcodeTokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) // 调整过滤器位置 .authenticationManager(reactiveAuthenticationManager()) // 响应式认证管理 .authorizeExchange() .pathMatchers("/api/hello").permitAll() .anyExchange().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().build(); } } ``` 2. **过滤器改造(关键步骤)** ```java @Component public class LowcodeTokenWebFilter implements WebFilter { private final ReactiveAuthenticationManager authenticationManager; public LowcodeTokenWebFilter(ReactiveAuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return Mono.justOrEmpty(extractToken(exchange.getRequest())) .flatMap(token -> authenticate(token)) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())); } private Mono<Authentication> authenticate(String token) { return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(token, token) ); } private String extractToken(ServerHttpRequest request) { // 实现token提取逻辑 return request.getHeaders().getFirst("X-AUTH-TOKEN"); } } ``` 3. **认证管理器适配** ```java @Bean public ReactiveAuthenticationManager reactiveAuthenticationManager( LowcodeTokenAuthenticationProvider provider ) { return authentication -> { Authentication auth = new UsernamePasswordAuthenticationToken( authentication.getPrincipal(), authentication.getCredentials() ); return Mono.just(provider.authenticate(auth)); }; } ``` **核心改造点说明**: 1. **架构差异处理**: - 用`SecurityWebFilterChain`代替`WebSecurityConfigurerAdapter` - 用`WebFilter`代替`Filter` - 使用`ReactiveAuthenticationManager`代替`AuthenticationProvider` 2. **执行顺序调整**: ```java // 替代原来的addFilterBefore .addFilterAt(lowcodeTokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) ``` - `SecurityWebFiltersOrder`定义了响应式过滤器的标准顺序 - 常用位置: - `SECURITY_CONTEXT_SERVER_WEB_EXCHANGE` (上下文初始化) - `AUTHENTICATION` (认证处理) - `AUTHORIZATION` (授权检查) 3. **认证流程改造**: ```mermaid sequenceDiagram Client->>Gateway: 请求 Gateway->>LowcodeTokenWebFilter: 触发过滤 LowcodeTokenWebFilter->>ReactiveAuthenticationManager: 认证请求 ReactiveAuthenticationManager->>LowcodeTokenAuthenticationProvider: 执行认证 LowcodeTokenAuthenticationProvider-->>Gateway: 返回认证结果 Gateway->>Client: 响应结果 ``` 4. **阻塞代码处理**: - 如果原有`LowcodeTokenAuthenticationProvider`包含阻塞操作: ```java // 使用调度器处理阻塞调用 return Mono.fromCallable(() -> blockingOperation()) .subscribeOn(Schedulers.boundedElastic()); ``` **注意事项**: 1. 响应式编程中禁止: - 使用ThreadLocal存储上下文 - 直接调用阻塞IO操作 - 同步方法调用未做隔离处理 2. 性能优化建议: ```java // 使用缓存优化token验证 private final LoadingCache<String, Authentication> tokenCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(1, TimeUnit.HOURS) .build(key -> provider.authenticate(...)); ``` 3. 异常处理增强: ```java .exceptionHandling() .authenticationEntryPoint((exchange, ex) -> { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().writeWith(...); }) .accessDeniedHandler((exchange, ex) -> { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().writeWith(...); }) ``` 完整迁移后,建议使用WebTestClient进行验证: ```java @Test void shouldReturnUnauthorizedWithoutToken() { webTestClient.get().uri("/api/resource") .exchange() .expectStatus().isUnauthorized(); } ```
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值