Spring 安全性:按名称注销、锁定或禁用用户

Spring Security: logout, lock, or disable user by name

我有一个使用 spring 安全性的测试应用程序,它有 3 个用户 [suzy、frank、julie]。我的应用程序将事件发送到外部安全分析系统。该系统分析事件以确定它是否是 "attack",然后应用程序轮询该系统以查看是否有任何事件被认为需要响应。这是一个例子:

  1. bob 执行操作 1(已发送事件)
  2. bob 执行(错误)2(已发送事件)
  3. bob 执行操作 3(已发送事件)
  4. 应用程序轮询外部系统并发现 bob 现在应该已注销
  5. bob 被注销(如何?)
  6. bob 的下一个请求被重定向到登录页面

一些注意事项:

我基本上希望我正在采取的任何行动都能对鲍勃的下一个请求产生影响。我想要这样的东西:

loadUserByUsername("bob").disable();

我现在希望建立的能力是:

我可以通过缓存用户会话然后使其无效来注销,尽管我更喜欢更好的方法(即 loadByUsername("bob").logout())。不过我不知道如何执行帐户禁用。

我希望尽可能不必构建核心 api 的自定义实现。我希望任何使用 spring 安全性的人都可以轻松集成此功能。

我会把它分解成你的两个问题:

以编程方式注销用户

不幸的是,我认为如果不以某种方式扩展核心 APIs,就没有办法做到这一点。幸运的是,与核心 APIs 的集成相当简单。

我为此概述了一些方法,因为我不确定 "background approach" 是否提供了很多优势(稍后会详细介绍)。

自定义 SecurityContextRepository

此时,我认为这是最好的做法。

后台线程方法(根据要求)很有意义,因此您在向应用程序发出请求时不必阻塞。但是,如果没有一些共享数据存储,您将无法轻松地与另一个线程(更不用说集群环境实例中另一台计算机上的另一个进程)进行通信了。

Spring 安全获取当前用户的方式正在使用 SecurityContextRepository 实现。默认实现从 HttpSession 获取用户。你可以做的是这样的:

public class SecurityAnalyzerSecurityContextRepository 
      implements SecurityContextRepository {

    private final SecurityAnalyzer securityAnalayzer;

    private final SecurityContextRepository delegate;

    public SecurityAnalyzerSecurityContextRepository(SecurityAnalyzer securityAnalyzer) {
        this(securityAnalyzer, new HttpSessionSecurityContextRepository());
    }

    public SecurityAnalyzerSecurityContextRepository(SecurityAnalyzer securityAnalayzer, SecurityContextRepository delegate) {
        this.securityAnalayzer = securityAnalyzer;
        this.delegate = delegate;
    }

    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        SecurityContext context = delegate.loadContext(requestResponseHolder);
        Authentication authentication = context.getAuthentication();
        if(authentication == null) {
            return context;
        }
        String principal = authentication.getName();

        // your SecurityAnalyzer implementation would need implement isEvil
        if(securityAnalyzer.isEvil(principal)) {
            return SecurityContextHolder.createEmptyContext();
        }
        return context;
    }

    public void saveContext(SecurityContext context, HttpServletRequest request,
            HttpServletResponse response) {
        delegate.saveContext(context, request, response);
    }

    public boolean containsContext(HttpServletRequest request) {
        return delegate.containsContext(request);
    }
}

我们的想法是,您可以委托给现有 SecurityContextRepository 并验证用户未被确定为邪恶。

或者,您可以提供一个 SecurityContextRepository 实现,该实现从后台线程可以写入的存储中加载。无论哪种方式,尽管有一个对商店的阻塞调用。

后台线程方法

注意:我不认为这是正确的方法,因此这个答案没有那么详细。

第一步是编写轮询外部系统的后台任务。显然SpringSecurity无法提供这一步,因为它不知道如何与外部系统进行交互。

此代码可能类似于:

SecurityAnalyzer analyzer = ...
Set<String> evilUsernames = analyzer.getAndRemoveNewEvilUsernames();
... what to do with evilUsernames? ...

现在的问题是如何处理邪恶的用户名。默认情况下 Spring 安全将从 HttpSession 获取当前用户。大多数 Servlet 容器默认 HttpSession 实现持久化到内存 Map。 Spring 安全无法获取对此 Map 的引用。

直接使用 JSESSIONID

一种选择是,如果我们能够获得每个用户的 JSESSIONID,我们可以发出这样的请求:

GET /j_spring_security_logout HTTP/1.1
Host: www.example.org
Cookie: JSESSIONID=<some-id>;

如果启用了 CSRF 保护,这会变得有点困难。要使其正常工作,您需要将 CsrfToken 公开为端点并首先调用以获取它:

GET /csrf HTTP/1.1
Host: www.example.org
Cookie: JSESSIONID=<some-id>;

然后使用响应,您可以使用令牌创建 POST:

POST /logout HTTP/1.1
Host: www.example.org
Cookie: JSESSIONID=<some-id>;

_csrf=<csrf-token>

此时你可能会问,我怎么知道JSESSIONID呢?一种选择是使用 Spring 安全性的 concurrency control。但是,开箱即用的实现不适用于集群实现。

最终,需要将信息传递到 SecurityAnalyzer 并从 SecurityAnalyzer return 编辑,或者自定义 Spring 安全性 API 将是必要的。

Spring 会话

一旦 Spring Session 添加了对 querying sessions by a user identifier 的支持,您就可以通过用户名使会话无效。

或者,如果 SecurityAnalyzer 知道会话 ID,那么 Spring 会话现在就可以工作了。

锁定帐户

您还需要了解如何锁定帐户。 Spring 安全性提供了一种支持锁定帐户的机制。

您可以使用 UserDetailsManager 实现将 UserDetails 更新为 return false for isAccountNonLocked。然后 Spring 安全的 DaoAuthenticationProvider 将利用 preAuthenticationChecks 确保帐户不被锁定。

如果您编写了自己的 UserDetailsService,那么内置 UserDetailsManager 实现将不起作用,因此您需要更新用户模型,以便自定义 UserDetailsService 创建一个UserDetailsService return 对于 isAccountNonLocked 是错误的。