Spring Security: 在删除用户时从用户中删除Cookies。

huangapple go评论65阅读模式
英文:

Spring security: delete cookies from user when I delete user

问题

我正在使用Spring Security和JSESSION cookie,因此每个用户在使用凭据登录后都会获得该cookie:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
  return http.build();
}

我创建了一个UserService来创建、检索、更新和删除用户,以及一个UserDetailsServiceImpl(它使用UserService)来实现UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserService userService;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return userService.findByUsername(username);
  }
}

因此,用户可以被删除。也许删除命令是由其他用户执行的,而不是由他自己执行,所以我没有会话上下文。

我想在删除用户时使与用户相关的所有cookie无效,这样他就不能再使用旧的cookie登录应用程序。

有什么方法吗?

提前感谢。

英文:

I'm using Spring security with JSESSION cookie so every user gets that cookie after its login with credentials:

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated().and().httpBasic()
    return http.build();
  }

I created a UserService to create, retrieve, update and delete users and UserDetailsServiceImpl (which uses UserService) to implement UserDetailsService:

  @Service
  public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserService userService;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return userService.findByUsername(username);
  }
}

So users can be deleted. Maybe the delete command is executed by other user rather than him so I don't have the session context.

I want to invalidate all cookies related to a user when I delete him so he cannot enter the app again using his old cookies.

Is there any way?

Thanks in advance.

答案1

得分: 2

你可以实现org.springframework.security.core.session.SessionRegistry或扩展默认实现org.springframework.security.core.session.SessionRegistryImpl以使用getAllSessions获取关联用户主体的所有会话,并根据需要使其失效。

要使会话注册表能够收到会话生命周期事件的通知,您需要在Servlet上下文中注册org.springframework.security.web.session.HttpSessionEventPublisher,示例如下:

@Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
    ServletListenerRegistrationBean<HttpSessionEventPublisher> listenerRegBean =  new ServletListenerRegistrationBean<>();
    listenerRegBean.setListener(new HttpSessionEventPublisher());
    return listenerRegBean;
}

@Bean
public SessionRegistry sessionRegistry(){
    return new SessionRegistryImpl();
}
英文:

You can implement org.springframework.security.core.session.SessionRegistry or extend the default implementation org.springframework.security.core.session.SessionRegistryImpl to use getAllSessions for the associated user principal and invalidate the appropriate ones as per your requirement.

For your session registry to be notified of the session life-cycle events, you need to register org.springframework.security.web.session.HttpSessionEventPublisher in Servlet context as below sample:

@Bean
public ServletListenerRegistrationBean&lt;HttpSessionEventPublisher&gt; httpSessionEventPublisher() {
ServletListenerRegistrationBean&lt;HttpSessionEventPublisher&gt; listenerRegBean =  new ServletListenerRegistrationBean&lt;&gt;();
           
listenerRegBean.setListener(new HttpSessionEventPublisher());
           return listenerRegBean;
}
    
    @Bean
    public SessionRegistry sessionRegistry(){
     return new SessionRegistryImpl();
    }

答案2

得分: 1

I got it working like so:

  1. 基于gs-securing-web
  2. 获取一些测试用户/修改:
     @Bean
     InMemoryUserDetailsManager userDetailsService() {
         UserDetails alice = User.withUsername("alice")
                 .password("{noop}alice")
                 .roles("ADMIN")
                 .build();
         UserDetails bob = User.withUsername("bob")
                 .password("{noop}bob")
                 .roles("USER")
                 .build();
         return new InMemoryUserDetailsManager(alice, bob);
     }
    
  3. hello.html添加(基本的管理员)控制:
    <form sec:authorize="hasRole('ROLE_ADMIN')" th:action="@{/invalidate}" method="post">
        <input type="text" name="user" />
        <input type="submit" value="Delete User" />
    </form>
    
  4. (基本的)"invalidate/delete user" 控制器:
    @Controller
    public class AdminController {
       @Autowired
       InMemoryUserDetailsManager userService;
       @Autowired
       SessionRegistry sessionRegistry; // !
    
       @PostMapping("/invalidate")
       public String invalidate(@RequestParam String user) {
          // 省略(任何)验证以保持简洁...
          // (可选)加载要使无效的用户:
          User bob = (User) userService.loadUserByUsername(user);
    
          // 所有“principals”:
          sessionRegistry.getAllPrincipals()
                 .stream()
                 // 过滤正确的用户名:
                 .filter(p -> p instanceof UserDetails prncp && prncp.getUsername().equals(bob.getUsername()))
                 // 使所有与principal相关的会话“过期”:
                 .forEach(p -> sessionRegistry.getAllSessions(p, false)
                    .forEach(SessionInformation::expireNow) // !
                 );
    
          // (可选)自定义用户删除/修改..
          // userService.updateUser(...);
    
          // (示例)自定义响应
          return "redirect:/hello";
       }
    }
    
  5. 要将所有内容连接在一起,我们(只)需要:
     @Bean
     SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         return http
                 .authorizeHttpRequests(...)
                 ...
                 .sessionManagement(sm -> sm // 1.
                         .maximumSessions(2) // <- 示例
                         .sessionRegistry(sessionRegistry()) // 2.
                         .expiredSessionStrategy(expireStrategy())) // 3.
                 .build();
     }
    
     @Bean // org.springframework.security.core.session.*:
     SessionRegistry sessionRegistry() { // 2a.
         return new SessionRegistryImpl();
     }
    
     @Bean //org.springframework.security.web.session.*:
     SessionInformationExpiredStrategy expireStrategy() { // 3a.
         // "/" 引用(自定义的会话使无效)重定向URL
         return new SimpleRedirectSessionInformationExpiredStrategy("/");
     }
    

测试时间:

  1. Bob 使用以下浏览器登录:

    • Chrome(匿名)
    • 和 Opera 浏览器

    .. 以显示 /hello

  2. Alice 使用 Chrome 登录,并提交 "invalidate form" 中的 bob

  3. 下次 bob 尝试请求任何(未安全/已安全)资源(来自任何浏览器)时,(Spring Security 默认链)ConcurrentSessionFilter 介入:

    • 检测到我们设置的会话过期
    • 执行注销(处理程序)
    • 并触发 SessionInformationExpiredEvent
  4. (.. 直到服务器重新启动/用户服务被重置)

对于负载平衡/分布式(服务器)环境:

我们可以确保“粘性会话”或
.. 作为更强大的解决方案,实现自定义的 SessionRegistry

  • 对于分发/负载平衡(会话必须是粘性的或)SessionRegistry 必须了解所有节点/实例/主体/会话
  • 为了(更)强大,我们需要从有效的、活动/持久的会话中预填充我们的 SessionRegistry。(我曾经在“devtools-reload”之后经历到它们为空,而底层(容器)会话仍然存在。)
英文:

I got it working like so

  1. Basing on gs-securing-web
  2. Get us some test users/modifying:
     @Bean
     InMemoryUserDetailsManager userDetailsService() {
     	UserDetails alice = User.withUsername(&quot;alice&quot;)
     			.password(&quot;{noop}alice&quot;)
     			.roles(&quot;ADMIN&quot;)
     			.build();
     	UserDetails bob = User.withUsername(&quot;bob&quot;)
     			.password(&quot;{noop}bob&quot;)
     			.roles(&quot;USER&quot;)
     			.build();
     	return new InMemoryUserDetailsManager(alice, bob);
     }
    
  3. Adding a (rudimentary admin) control to hello.html:
    &lt;form sec:authorize=&quot;hasRole(&#39;ROLE_ADMIN&#39;)&quot; th:action=&quot;@{/invalidate}&quot; method=&quot;post&quot;&gt;
        &lt;input type=&quot;text&quot; name=&quot;user&quot; /&gt;
        &lt;input type=&quot;submit&quot; value=&quot;Delete User&quot; /&gt;
    &lt;/form&gt;
    
  4. The (rudimentary) "invalidate/delete user" controller:
    @Controller
    public class AdminController {
       @Autowired
       InMemoryUserDetailsManager userService;
       @Autowired
       SessionRegistry sessionRegistry; // !
    
       @PostMapping(&quot;/invalidate&quot;)
       public String invalidate(@RequestParam String user) {
          // omitted (any) validation for brevity...
          // (optional) load user to invalidate:
          User bob = (User) userService.loadUserByUsername(user);
    
          // all &quot;principals&quot;:
          sessionRegistry.getAllPrincipals()
                 .stream()
                 // filter correct user name:
                 .filter(p -&gt; p instanceof UserDetails prncp &amp;&amp; prncp.getUsername().equals(bob.getUsername()))
                 // &quot;expire&quot; all sessions for principal:
                 .forEach(p -&gt; sessionRegistry.getAllSessions(p, false)
                    .forEach(SessionInformation::expireNow) // !
                 );
    
          // (optional) custom user deletion/modification..
          // userService.updateUser(...);
    
          // (sample) custom response
          return &quot;redirect:/hello&quot;;
       }
    }
    
  5. To wire it all up, we (only) need:
     @Bean
     SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
     	return http
     			.authorizeHttpRequests(...)
                 ...
     			.sessionManagement(sm -&gt; sm // 1.
     					.maximumSessions(2) // &lt;- example
     					.sessionRegistry(sessionRegistry()) // 2.
     					.expiredSessionStrategy(expireStrategy())) // 3.
     			.build();
     }
    
     @Bean // org.springframework.security.core.session.*:
     SessionRegistry sessionRegistry() { // 2a.
     	return new SessionRegistryImpl();
     }
    
     @Bean //org.springframework.security.web.session.*:
     SessionInformationExpiredStrategy expireStrategy() { // 3a.
         // &quot;/&quot; refers to (custom, session invalidation) redirect url
     	return new SimpleRedirectSessionInformationExpiredStrategy(&quot;/&quot;);
     }
    

Testing Time

  1. Bob logs in with:

    • Chrome (anonymous)
    • and Opera browser

    .. to display /hello

  2. Alice logs in (with chrome), and submits bob with "invalidate form"

  3. Next time, when bob tries to request any (un-/secured) resource (from any browser), (spring security default chain) ConcurrentSessionFilter jumps in:

    • detects session expiry (set by us)
    • performs logout (handler)
    • and "fires" a SessionInformationExpiredEvent
  4. (..until server restarts/user service is reset)

For a load balanced/distributed (server) environment

We can ensure "sticky sessions" or
..as for a more resilient solution, implement custom SessionRegistry.

  • For distribution/load balancing the (sessions must be sticky or) SessionRegistry must be aware of all nodes/instances/principals/sessions
  • for (more) resilience, we need to pre-populate our SessionRegistry from valid, active/persistent Sessions. (I experienced them empty after a "devtools-reload", while the underlying (container) sessions still persisted.)

huangapple
  • 本文由 发表于 2023年6月6日 16:54:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76412956.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定