自定义认证(Authentication)的存储位置

默认情况下, Spring Security 在 HTTP 会话中为你存储 security context。然而,这里有几个原因,你可能想自定义:

  • 你可能想在 HttpSessionSecurityContextRepository 实例上调用单个 setter

  • 你可能想在缓存或数据库中存储 security context,以实现横向扩展。

首先,你需要创建一个 SecurityContextRepository 的实现,或者使用一个现有的实现,如 HttpSessionSecurityContextRepository

package com.example.eochadmin.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.context.DeferredSecurityContext;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;

public class MySecurityContextRepository implements SecurityContextRepository {
    private static final String SESSION_ATTR_NAME = "SPRING_SECURITY_CONTEXT";
    /*
        用来从存储中加载安全上下文,通常情况下,你会在这里实现逻辑来根据请求的信息加载和返回相应的安全上下文对象
     */
    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        /*
        首先获取请求中的 HttpSession 对象,如果存在会话则尝试获取名为 SPRING_SECURITY_CONTEXT 的属性,
        即安全上下文对象。如果会话不存在或者属性为空,则返回 null
         */
        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpSession session = request.getSession(false); // Do not create session if it doesn't exist
        if (session != null) {
            return (SecurityContext) session.getAttribute(SESSION_ATTR_NAME);
        } else {
            return null;
        }
    }

    /*它的作用是通过请求对象加载延迟的安全上下文。在这里,
    实现调用了 SecurityContextRepository.super.loadDeferredContext(request),这将委托给接口的默认实现来处理。
    通常情况下,你可以在这里定制化实现,根据具体的需求来加载延迟的安全上下文
     */
    @Override
    public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
        return SecurityContextRepository.super.loadDeferredContext(request);
    }

    /*
    这个方法用来保存安全上下文到存储中。在当前代码中,方法体是空的,没有实际的保存操作。通常你会在这里编写逻辑,
    将给定的安全上下文对象保存到某种持久化存储中,以便后续的访问可以获取到正确的安全上下文信息
     */
    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        /*
        首先获取请求中的 HttpSession 对象,如果不存在会话则创建一个新的会话。
        然后将安全上下文对象存储在会话的 SPRING_SECURITY_CONTEXT 属性中
         */
        HttpSession session = request.getSession(true); // Create session if it doesn't exist
        session.setAttribute(SESSION_ATTR_NAME, context);
    }

    /*
    这个方法用来检查请求中是否包含有效的安全上下文。在这里的实现中,直接返回了 false,
    表示不包含有效的安全上下文。通常情况下,你需要在这里实现逻辑来检查给定请求是否有相关的安全上下文信息
     */
    @Override
    public boolean containsContext(HttpServletRequest request) {
        /*
        首先获取请求中的 HttpSession 对象,如果会话存在且会话中的 SPRING_SECURITY_CONTEXT 属性不为空,
        则返回 true;否则返回 false
         */
        HttpSession session = request.getSession(false); // Do not create session if it doesn't exist
        return session != null && session.getAttribute(SESSION_ATTR_NAME) != null;
    }
}

然后你可以在 HttpSecurity 中设置它。

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        MySecurityContextRepository repo = new MySecurityContextRepository();
        http.securityContext((context) -> context.securityContextRepository(repo));
        return http.build();
    }
}

在上面的示例中,确实是通过 HttpSession 存储了安全上下文(即 SecurityContext 对象),而不是直接存储认证用户的详细信息。让我来进一步解释:

1. 存储的内容

HttpSession 中存储的是 SecurityContext 对象,而 SecurityContext 包含了 Authentication 对象,后者表示认证用户的详细信息(例如用户名、权限等)。

2. SecurityContext 中的 Authentication 对象

SecurityContext 是 Spring Security 中用来持有当前用户的信息的容器。

Authentication 对象包含了认证用户的详细信息,它是 Principal(主体)和 GrantedAuthority(授权信息)的封装。

3. 存储过程

- 在上述示例中,`saveContext` 方法将整个 SecurityContext 对象存储在 HttpSession 中的 SPRING_SECURITY_CONTEXT 属性中。

- 这意味着在会话中,我们可以通过 SecurityContext 对象来获取 Authentication 对象,进而获取认证用户的详细信息。

4. 认证用户信息的存储

- 认证用户的具体信息(例如用户名、权限)通常包含在 Authentication 对象中。

- Spring Security 在认证成功后,会将有效的 Authentication 对象设置到 SecurityContext 中,然后由 saveContext 方法负责将整个 SecurityContext 存储在 HttpSession

因此,虽然代码示例中直接操作的是 SecurityContext,实际上 SecurityContext 中包含了 Authentication 对象,从而间接地存储了认证用户的信息。这种设计符合了 Spring Security 的认证和授权机制,确保了安全上下文和认证信息的正确管理和使用。

手动存储 Authentication

例如,在某些情况下,你可能要手动验证用户,而不是依靠  Spring Security filter。你可以使用自定义 filter 或 Spring MVC controller 端点来做到这一点。如果你想在请求之间保存 认证,例如在 HttpSession 中,你就必须这样做

private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();

    @PostMapping("/login")
    public void login(@RequestParam("username") String username,@RequestParam("password") String password, HttpServletRequest request, HttpServletResponse response) {
        UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
                username, password);
        MyAuthenticationManager authenticationManager = new MyAuthenticationManager();
        Authentication authentication = authenticationManager.authenticate(token);
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        securityContextRepository.saveContext(context, request, response);
    }
  1. SecurityContextRepository 添加到 controller 中

  2. 注入 HttpServletRequestHttpServletResponse,以便能够保存 SecurityContext

  3. 使用提供的凭证创建一个未经 认证的UsernamePasswordAuthenticationToken

  4. 调用 AuthenticationManager#authenticate 来验证用户

  5. 创建一个 SecurityContext,并在其中设置 Authentication

  6. SecurityContextRepository 中保存 SecurityContext

配置无状态认证(Authentication)的持久化

有时不需要创建和维护一个 HttpSession,例如,在不同的请求中坚持认证。一些认证机制,如 HTTP Basic 是无状态的,因此,在每次请求时都会重新认证用户。

如果你不希望创建会话,你可以使用 SessionCreationPolicy.STATELESS,像这样

http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));// 配置为无状态会话
        

上述配置是将 SecurityContextRepository 配置 为使用 NullSecurityContextRepository,同时也是为了 防止请求被保存在会话中

配置并发会话控制

如果你希望对单个用户登录你的应用程序的能力进行限制, Spring Security 支持开箱即用,只需添加以下简单内容。首先,你需要在你的配置中添加以下 listener,以保持  Spring Security 对会话生命周期事件的更新

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
        );
    return http.build();
}

会话会自行过期,不需要做任何事情来确保 security context 被删除。也就是说, Spring Security 可以检测到会话过期的情况,并采取你指定的具体行动。例如,当用户用已经过期的会话发出请求时,你可能想重定向到一个特定的端点。这可以通过 HttpSecurity 中的 invalidSessionUrl 实现

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .invalidSessionUrl("/invalidSession")
        );
    return http.build();
}

请注意,如果你使用这种机制来检测会话超时,如果用户注销后没有关闭浏览器又重新登录,它可能会错误地报告一个错误。这是因为当你使会话失效时,session cookie 没有被清除,即使用户已经注销,也会重新提交。如果你的情况是这样,你可能想 配置注销来清除 session cookie。

定制失效会话的策略

invalidSessionUrl 是使用 SimpleRedirectInvalidSessionStrategy 实现 来设置 InvalidSessionStrategy 的方便方法。如果你想自定义行为,你可以实现 InvalidSessionStrategy 接口并使用 invalidSessionStrategy 方法进行配置

注销时清除 Session Cookies

你可以在注销时明确地删除 JESSIONID cookie,例如通过使用 logout handler 中的 Clear-Site-Data header:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout((logout) -> logout
            .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
        );
    return http.build();
}

Copied!

这样做的好处是与容器无关,可以与任何支持 Clear-Site-Data header 的容器一起工作。

作为一种替代方法,你也可以在 logout handler 中使用以下语法:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout(logout -> logout
            .deleteCookies("JSESSIONID")
        );
    return http.build();
}

Copied