登录用户数据获取

登录成功之后,在后续的业务逻辑中,开发者可能还需要获取登录成功的用户对象,如果不使用任何安全管理框架,那么可以将用户信息保存在 HttpSession 中,以后需要的时候直接从 HttpSession 中获取数据。在 Spring Security 中,用户登录信息本质上还是保存在 HttpSession 中,但是为了方便使用, Spring Security 对 HttpSession 中的用户信息进行了封装,封装之后,开发者若再想获取用户登录数据就会有两种不同的思路:

  • 从 SecurityContextHolder 中获取。

  • 从当前请求对象中获取。

这里列出来的两种方式是主流的做法,开发者也可以使用一些非主流的方式获取登录成功后的用户信息,例如直接从 HttpSession 中获取用户登录数据。

无论是哪种获取方式,都离不开一个重要的对象: Authentication。 在 Spring Security 中,Authentication 对象主要有两方面的功能:

  1. 作为 AuthenticationManager 的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的 isAuthenticated 方法返回 false,表示用户还未认证。

  2. 代表已经经过身份认证的用户,此时的 Authentication 可以从 SecurityContext 中获取。

一个 Authentication 对象主要包含三个方面的信息:

  1. principal:定义认证的用户。如果用户使用用户名/密码的方式登录,principal 通常就是一个 UserDetails 对象。

  2. credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防止泄漏。

  3. authorities:用户被授予的权限信息。

Java 中本身提供了 Principal 接口用来描述认证主体, Principal 可以代表一个公司、个人或者登录 ID。 Spring Security 中定义了 Authentication 接口用来规范登录用户信息,Authentication 继承自 Principal:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated);
}

这里接口中定义的方法都很好理解:

  • getAuthorities 方法:用来获取用户权限。

  • getCredentials 方法:用来获取用户凭证,一般来说就是密码。

  • getDetails 方法:用来获取用户的详细信息,可能是当前的请求乊类。

  • getPrincipal 方法:用来获取当前用户信息,可能是一个用户名,也可能是一个用户对象。

  • isAuthenticated 方法:当前用户是否认证成功。

可以看到,在 Spring Security 中,只要获取到 Authentication 对象,就可以获取到登录用户的详细信息。

不同的认证方式对应不同的 Authentication 实例,Spring Security 中的 Authentication 实现类如图 2-11 所示。

这些实现类现看起来可能会觉得陌生,不过没关系,在后续的章节中,这些实现类基本上都会涉及。现在我们先对每个类的功能做一个大概介绍:

  1. AbstractAuthenticationToken:该类实现了 Authentication 和 CredentialsContainer 两个接口,在 AbstractAuthenticationToken 中对 Authentication 接口定义的各个数据获取方法进行了实现, CredentialsContainer 则提供了登录凭证擦除方法。一般在登录成功后,为了防止用户信息泄漏,可以将登录凭证(例如密码)擦除。

  2. RememberMeAuthenticationToken:如果用户使用 RememberMe 的方式登录,登录信息将封装在 RememberMeAuthenticationToken 中。

  3. TestingAuthenticationToken:单元测试时封装的用户对象。

  4. AnonymousAuthenticationToken:匿名登录时封装的用户对象。

  5. RunAsUserToken:替换验证身份时封装的用户对象。

  6. UsernamePasswordAuthenticationToken:表单登录时封装的用户对象。

  7. JaasAuthenticationToken:JAAS 认证时封装的用户对象。

  8. PreAuthenticatedAuthenticationToken: Pre-Authentication 场景下封装的用户对象。

在这些 Authentication 的实例中,最常用的有两个: UsernamePasswordAuthenticationToken 和 RememberMeAuthenticationToken。在 2.1 节中的案例对应的用户认证对象就是 UsernamePasswordAuthenticationToken。

了解了 Authentication 对象之后,接下来我们来看一下如何在登录成功后获取用户登录信息,即 Authentication 对象。

image 2024 04 11 08 51 37 730
Figure 1. 图 2-11 Authentication 的实现类

从 SecurityContextHolder 中获取

我们在 2.2 节案例的基础上,再添加一个 UserController,内容如下:

@RestController
public class UserController {
    @GetMapping("/user")
    public void userInfo() {
        Authentication authentication =
                SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        System.out.println("name = " + name);
        System.out.println("authorities = " + authorities);
    }
}

配置完成后,启动项目,登录成功后,访问 /user 接口,控制台就会打印出登录用户信息,当然,由于我们目前没有给用户配置角色,所以默认的用户角色为空数组,如图 2-12 所示。

image 2024 04 11 08 53 41 529
Figure 2. 图 2-12 登录成功后打印出来的用户名和用户角色

这里为了演示方便,我们在 Controller 中获取登录用户信息,可以发现, SecurityContextHolder.getContext() 是一个静态方法,也就意味着我们随时随地都可以获取到登录用户信息,在 service 层也可以获取到登录用户信息(在实际项目中,大部分情况下也都是在 service 层获取登录用户信息)。

获取登录用户信息的代码很简单,那么 SecurityContextHolder 到底是什么?它里边的数据又是从何而来的?接下来我们将进行一一解析。

SecurityContextHolder

SecurityContextHolder 中存储的是 SecurityContext, SecurityContext 中存储的则是 Authentication,三者的关系如图 2-13 所示。

image 2024 04 11 09 30 13 339
Figure 3. 图 2-13 SecurityContextHolder、 SecurityContext 以及 Authentication 之间的关系

这幅图清晰地描述了 SecurityContextHolder、 SecurityContext 以及 Authentication 三者之间的关系。

首先在 SecurityContextHolder 中存放的是 SecurityContext, SecurityContextHolder 中定义了三种不同的数据存储策略,这实际上是一种典型的策略模式:

  1. MODE_THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,大家知道 ThreadLocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 Web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。

  2. MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。

  3. MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中, 在 Java Web 开发中,这种模式很少使用到。

Spring Security 中定义了 SecurityContextHolderStrategy 接口用来规范存储策略中的方法,我们来看一下:

public interface SecurityContextHolderStrategy {
    void clearContext();

    SecurityContext getContext();

    void setContext(SecurityContext context);

    SecurityContext createEmptyContext();
}

接口中一共定义了四个方法:

  1. clearContext:该方法用来清除存储的 SecurityContext 对象。

  2. getContext:该方法用来获取存储的 SecurityContext 对象。

  3. setContext:该方法用来设置存储的 SecurityContext 对象。

  4. createEmptyContext:该方法则用来创建一个空的 SecurityContext 对象。

在 Spring Security 中, SecurityContextHolderStrategy 接口一共有三个实现类,对应了三种不同的存储策略,如图 2-14 所示。

image 2024 04 11 09 39 28 762
Figure 4. 图 2-14 SecurityContextHolderStrategy 的三个实现类

每一个实现类都对应了不同的实现策略,我们先来看一下 ThreadLocalSecurityContextHolderStrategy:

final class ThreadLocalSecurityContextHolderStrategy implements
        SecurityContextHolderStrategy {
    private static final ThreadLocal<SecurityContext> contextHolder =
            new ThreadLocal<>();

    public void clearContext() {
        contextHolder.remove();
    }

    public SecurityContext getContext() {
        SecurityContext ctx = contextHolder.get();
        if (ctx == null) {
            ctx = createEmptyContext();
            contextHolder.set(ctx);
        }
        return ctx;
    }

    public void setContext(SecurityContext context) {
        contextHolder.set(context);
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

ThreadLocalSecurityContextHolderStrategy 实现了 SecurityContextHolderStrategy 接口,并实现了接口中的方法,存储数据的载体就是一个 ThreadLocal,所以针对 SecurityContext 的清空、获取以及存储,都是在 ThreadLocal 中进行操作,例如清空就是调用 ThreadLocal 的 remove 方法。SecurityContext 是一个接口,它只有一个实现类 SecurityContextImpl,所以创建就直接新建一个 SecurityContextImpl 对象即可。

再来看 InheritableThreadLocalSecurityContextHolderStrategy:

final class InheritableThreadLocalSecurityContextHolderStrategy
        implements SecurityContextHolderStrategy {
    private static final ThreadLocal<SecurityContext> contextHolder =
            new InheritableThreadLocal<>();

    public void clearContext() {
        contextHolder.remove();
    }

    public SecurityContext getContext() {
        SecurityContext ctx = contextHolder.get();
        if (ctx == null) {
            ctx = createEmptyContext();
            contextHolder.set(ctx);
        }
        return ctx;
    }

    public void setContext(SecurityContext context) {
        contextHolder.set(context);
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

InheritableThreadLocalSecurityContextHolderStrategy 和 ThreadLocalSecurityContextHolderStrategy 的实现策略基本一致,不同的是存储数据的载体变了,在 InheritableThreadLocalSecurityContextHolderStrategy 中存储数据的载体变成了 InheritableThreadLocal。 InheritableThreadLocal 继承自 ThreadLocal,但是多了一个特性,就是在子线程创建的一瞬间,会自动将父线程中的数据复制到子线程中。该存储策略正是利用了这一特性,实现了在子线程中获取登录用户信息的功能。

最后再来看一下 GlobalSecurityContextHolderStrategy:

final class GlobalSecurityContextHolderStrategy implements
        SecurityContextHolderStrategy {
    private static SecurityContext contextHolder;

    public void clearContext() {

        contextHolder = null;
    }

    public SecurityContext getContext() {
        if (contextHolder == null) {
            contextHolder = new SecurityContextImpl();
        }
        return contextHolder;
    }

    public void setContext(SecurityContext context) {
        contextHolder = context;
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

GlobalSecurityContextHolderStrategy 的实现就更简单了,用一个静态变量来保存 SecurityContext,所以它也可以在多线程环境下使用。但是一般在 Web 开发中,这种存储策略使用得较少。

最后我们再来看一下 SecurityContextHolder 的源码:

public class SecurityContextHolder {
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

    static {
        initialize();
    }

    public static void clearContext() {
        strategy.clearContext();
    }

    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    public static int getInitializeCount() {
        return initializeCount;
    }

    private static void initialize() {
        if (!StringUtils.hasText(strategyName)) {
            strategyName = MODE_THREADLOCAL;
        }
        if (strategyName.equals(MODE_THREADLOCAL)) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals(MODE_GLOBAL)) {
            strategy = new GlobalSecurityContextHolderStrategy();
        } else {
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy =
                        (SecurityContextHolderStrategy) customStrategy.newInstance();
            } catch (Exception ex) {
                ReflectionUtils.handleReflectionException(ex);
            }
        }
        initializeCount++;
    }

    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

    public static void setStrategyName(String strategyName) {
        SecurityContextHolder.strategyName = strategyName;
        initialize();
    }

    public static SecurityContextHolderStrategy getContextHolderStrategy() {
        return strategy;
    }

    public static SecurityContext createEmptyContext() {
        return strategy.createEmptyContext();
    }
}

从这段源码中可以看到,SecurityContextHolder 定义了三个静态常量用来描述三种不同的存储策略;存储策略 strategy 会在静态代码块中进行初始化,根据不同的 strategyName 初始化不同的存储策略;strategyName 变量表示目前正在使用的存储策略,开发者可以通过配置系统变量或者调用 setStrategyName 来修改SecurityContextHolder 中的存储策略,调用 setStrategyName 后会重新初始化 strategy。

默认情况下,如果开发者试图从子线程中获取当前登录用户数据,就会获取失败,代码如下:

@RestController
public class UserController {
    @GetMapping("/user")
    public void userInfo() {

        Authentication authentication =
                SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        Collection<? extends GrantedAuthority> authorities =
                authentication.getAuthorities();
        System.out.println("name = " + name);
        System.out.println("authorities = " + authorities);

        new Thread(new Runnable() {
            @Override
            public void run() {
                Authentication authentication =
                        SecurityContextHolder.getContext().getAuthentication();
                if (authentication == null) {
                    System.out.println("获取用户信息失败");
                    return;
                }
                String name = authentication.getName();
                Collection<? extends GrantedAuthority> authorities =
                        authentication.getAuthorities();
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + ":name = " + name);
                System.out.println(threadName + ":authorities = " + authorities);
            }
        }).start();
    }
}

在子线程中尝试获取登录用户数据时,获取到的数据为 null,如图 2-15 所示。

image 2024 04 11 09 53 45 099
Figure 5. 图 2-15 子线程获取登录用户信息为 null

子线程之所以获取不到登录用户信息,就是因为数据存储在 ThreadLocal 中,存储和读取不是同一个线程,所以获取不到。如果希望子线程中也能够获取到登录用户信息,可以将 SecurityContextHolder 中的存储策略改为 MODE_INHERITABLETHREADLOCAL,这样就支持多线程环境下获取登录用户信息了。

默认的存储策略是通过 System.getProperty 加载的,因此我们可以通过配置系统变量来修改默认的存储策略,以 IntelliJ IDEA 为例,首先单击启动按钮,选择 Edit Configurations 按钮,如图 2-16 所示,然后在打开的选项中,配置 VM options 参数,添加如下一行, 配置界面如图 2-17 所示。

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
image 2024 04 11 09 55 27 051
Figure 6. 图 2-17 配置 SecurityContextHolder 中的存储策略

这样,在 SecurityContextHolder 中通过 System.getProperty 加载到的默认存储策略就支持多线程环境了。

配置完成之后,再次启动项目,此时访问 /user 接口,即使在子线程中,也可以获取到登录用户信息了,如图 2-18 所示。

image 2024 04 11 09 56 13 656
Figure 7. 图 2-18 子线程中也可以获取到登录用户信息

看到这里读者不禁要问了,既然 SecurityContextHolder 默认是将用户信息存储在 ThreadLocal 中,在 Spring Boot 中不同的请求都是由不同的线程处理的,那为什么每一次请求都还能从 SecurityContextHolder 中获取到登录用户信息呢?这就不得不提到 Spring Security 过滤器链中重要的一环——SecurityContextPersistenceFilter。

SecurityContextPersistenceFilter

前面介绍了 Spring Security 中的常见过滤器,在这些过滤器中,存在一个非常重要的过滤器就是 SecurityContextPersistenceFilter。

默认情况下,在 Spring Security 过滤器链中,SecurityContextPersistenceFilter 是第二道防线,位于 WebAsyncManagerIntegrationFilter 之后。从 SecurityContextPersistenceFilter 这个过滤器的名字上就可以推断出来,它的作用是为了存储 SecurityContext 而设计的。

整体上来说,SecurityContextPersistenceFilter 主要做两件事情:

  1. 当一个请求到来时,从 HttpSession 中获取 SecurityContext 并存入 SecurityContextHolder 中,这样在同一个请求的后续处理过程中,开发者始终可以通过 SecurityContextHolder 获取到当前登录用户信息。

  2. 当一个请求处理完毕时, 从 SecurityContextHolder 中获取 SecurityContext 并存入 HttpSession 中(主要针对异步 Servlet),方便下一个请求到来时,再从 HttpSession 中拿出来使用,同时擦除 SecurityContextHolder 中的登录用户信息。

在 SecurityContextPersistenceFilter 过滤器中,当一个请求处理完毕时,从 SecurityContextHolder 中获取 SecurityContext 存入 HttpSession 中,这一步的操作主要是针对异步 Servlet。如果不是异步 Servlet,在响应提交时,就会将 SecurityContext 保存到 HttpSession 中了,而不会等到在 SecurityContextPersistenceFilter 过滤器中再去存储。

这就是 SecurityContextPersistenceFilter 大致上做的事情,在正式开始介绍 SecurityContextPersistenceFilter 之前,需要先介绍另外一个接口,这就是 SecurityContextRepository 接口。

将 SecurityContext 存入 HttpSession,或者从 HttpSession 中加载数据并转为 SecurityContext 对象,这些事情都是由 SecurityContextRepository 接口的实现类完成的,因此这里我们就先从 SecurityContextRepository 接口开始看起。

首先我们来看一下 SecurityContextRepository 接口的定义:

public interface SecurityContextRepository {
    SecurityContext loadContext(HttpRequestResponseHolder holder);

    void saveContext(SecurityContext context, HttpServletRequest request,
                     HttpServletResponse response);

    boolean containsContext(HttpServletRequest request);
}

SecurityContextRepository 接口中一共定义了三个方法:

  1. loadContext:这个方法用来加载 SecurityContext 对象出来,对于没有登录的用户,这里会返回一个空的 SecurityContext 对象,注意空的 SecurityContext 对象是指 SecurityContext 中不存在 Authentication 对象,而不是该方法返回 null。

  2. saveContext:该方法用来保存一个 SecurityContext 对象。

  3. containsContext:该方法可以判断 SecurityContext 对象是否存在。

在 Spring Security 框架中,为 SecurityContextRepository 接口一共提供了三个实现类,如图 2-19 所示。

image 2024 04 11 10 04 02 480
Figure 8. 图 2-19 SecurityContextRepository 接口的三个实现类

在这三个实现类中,TestSecurityContextRepository 为单元测试提供支持;NullSecurityContextRepository 实现类中,loadContext 方法总是返回一个空的 SecurityContext 对象,saveContext 方法未做任何实现,containsContext 方法总是返回 false,所以 NullSecurityContextRepository 实现类实际上未做 SecurityContext 的存储工作。

在 Spring Security 中默认使用的实现类是 HttpSessionSecurityContextRepository,通过 HttpSessionSecurityContextRepository 实现了将 SecurityContext 存储到 HttpSession 以及从 HttpSession 中加载 SecurityContext 出来。这里我们来重点看一下 HttpSessionSecurityContextRepository 类。

在正式开始介绍 HttpSessionSecurityContextRepository 之前,首先来看一下 HttpSessionSecurityContextRepository 中定义的关于请求和封装的两个内部类。

首先是 HttpSessionSecurityContextRepository 中定义的对于响应的封装类 SaveToSessionResponseWrapper,我们先来看一下 SaveToSessionResponseWrapper 的继承关系图,如图 2-20 所示。

image 2024 04 11 10 08 12 789
Figure 9. 图 2-20 SaveToSessionResponseWrapper 继承关系图

从这幅继承关系图中可以看到,SaveToSessionResponseWrapper 实际上就是我们所熟知的 HttpServletResponse 功能的扩展。这里有三个关键的实现类:

  1. HttpServletResponseWrapper: HttpServletResponseWrapper 实现了 HttpServletResponse 接口,它是 HttpServletResponse 的装饰类,利用 HttpServletResponseWrapper 可以方便地操作参数和输出流等。

  2. OnCommittedResponseWrapper: OnCommittedResponseWrapper 继承自 HttpServletResponseWrapper,对其功能进行了增强,最重要的增强在于可以获取 HttpServletResponse 的提交行为。当 HttpServletResponse 的 sendError、 sendRedirect、 flushBuffer、flush 以及 close 等方法被调用时, onResponseCommitted 方法会被触发,开发者可以在 onResponseCommitted 方法中做一些数据保存操作,例如保存 SecurityContext。不过 OnCommittedResponseWrapper 中的 onResponseCommitted 方法只是一个抽象方法,并没有具体的实现,具体的实现则在它的实现类 SaveContextOnUpdateOrErrorResponseWrapper 中。

  3. SaveContextOnUpdateOrErrorResponseWrapper:该类继承自 OnCommittedResponseWrapper 并对 onResponseCommitted 方法做了实现。在 SaveContextOnUpdateOrErrorResponseWrapper 类中声明了一个 contextSaved 变量,表示 SecuirtyContext 是否已经存储成功。当 HttpServletResponse 提交时,会调用 onResponseCommitted 方法,在 onResponseCommitted 方法中调用 saveContext 方法,将 SecurityContext 保存到 HttpSession 中,同时将 contextSaved 变量标记为 true。 saveContext 方法在这里也是一个抽象方法,具体的实现则在 SaveToSessionResponseWrapper 类中。

接下来看一下 HttpSessionSecurityContextRepository 中 SaveToSessionResponseWrapper 的定义:

final class SaveToSessionResponseWrapper extends
        SaveContextOnUpdateOrErrorResponseWrapper {
    private final HttpServletRequest request;
    private final boolean httpSessionExistedAtStartOfRequest;
    private final SecurityContext contextBeforeExecution;
    private final Authentication authBeforeExecution;

    SaveToSessionResponseWrapper(HttpServletResponse response,
                                 HttpServletRequest request,
                                 boolean httpSessionExistedAtStartOfRequest,
                                 SecurityContext context) {
        super(response, disableUrlRewriting);
        this.request = request;
        this.httpSessionExistedAtStartOfRequest =
                httpSessionExistedAtStartOfRequest;
        this.contextBeforeExecution = context;
        this.authBeforeExecution = context.getAuthentication();
    }

    @Override
    protected void saveContext(SecurityContext context) {

        final Authentication authentication = context.getAuthentication();
        HttpSession httpSession = request.getSession(false);
        if (authentication == null ||
                trustResolver.isAnonymous(authentication)) {
            if (httpSession != null && authBeforeExecution != null) {
                httpSession.removeAttribute(springSecurityContextKey);
            }
            return;
        }
        if (httpSession == null) {
            httpSession = createNewSessionIfAllowed(context);
        }
        if (httpSession != null) {
            if (contextChanged(context)
                    ||
                    httpSession.getAttribute(springSecurityContextKey) == null) {
                httpSession.setAttribute(springSecurityContextKey, context);
            }
        }
    }

    private boolean contextChanged(SecurityContext context) {
        return context != contextBeforeExecution
                || context.getAuthentication() != authBeforeExecution;
    }

    private HttpSession createNewSessionIfAllowed(SecurityContext context) {
        if (isTransientAuthentication(context.getAuthentication())) {
            return null;
        }
        if (httpSessionExistedAtStartOfRequest) {
            return null;
        }
        if (!allowSessionCreation) {
            return null;
        }
        if (contextObject.equals(context)) {
            return null;
        }
        try {
            return request.getSession(true);
        } catch (IllegalStateException e) {
        }
        return null;
    }
}

在 SaveToSessionResponseWrapper 中其实主要定义了三个方法: saveContext、contextChanged 以及 createNewSessionIfAllowed。

  1. saveContext:该方法主要是用来保存 SecurityContext,如果 authentication 对象为 null 或者它是一个匿名对象,则不需要保存 SecurityContext(参见 SEC-776: https://github.com/spring-projects/spring-security/issues/1036 );同时,如果 httpSession 不为 null 并且 authBeforeExecution 也不为 null,就从 httpSession 中将保存的登录用户数据移除,这个主要是为了防止开发者在注销成功的回调中继续调用 chain.doFilter 方法, 进而导致原始的登录信息无法清除的问题(参见 SEC-1587: https://github.com/spring-projects/spring-security/issues/1826 );如果 httpSession 为 null,则去创建一个 HttpSession 对象;最后,如果 SecurityContext 发生了变化,或者 httpSession 中没有保存 SecurityContext,则调用 httpSession 中的 setAttribute 方法将 SecurityContext 保存起来。

  2. contextChanged:该方法主要用来判断 SecurityContext 是否发生变化,因为在程序运行过程中,开发者可能修改了 SecurityContext 中的 Authentication 对象。

  3. createNewSessionIfAllowed:该方法用来创建一个 HttpSession 对象。

这就是 HttpSessionSecurityContextRepository 中封装的 SaveToSessionResponseWrapper 对象,一个核心功能就是在 HttpServletResponse 提交的时候,将 SecurityContext 保存到HttpSession 中。

接下来看一下 HttpSessionSecurityContextRepository 中关于 SaveToSessionRequestWrapper 的定义, SaveToSessionRequestWrapper 相对而言就要简单很多了:

private static class SaveToSessionRequestWrapper extends
        HttpServletRequestWrapper {
    private final SaveContextOnUpdateOrErrorResponseWrapper response;

    SaveToSessionRequestWrapper(HttpServletRequest request,
                                SaveContextOnUpdateOrErrorResponseWrapper response) {
        super(request);
        this.response = response;
    }

    @Override
    public AsyncContext startAsync() {
        response.disableSaveOnResponseCommitted();
        return super.startAsync();
    }

    @Override
    public AsyncContext startAsync(ServletRequest servletRequest,
                                   ServletResponse servletResponse) throws IllegalStateException {
        response.disableSaveOnResponseCommitted();
        return super.startAsync(servletRequest, servletResponse);
    }
}

SaveToSessionRequestWrapper 类实际上是在 Spring Security 3.2 之后出现的封装类,在 Spring Security 3.2 之前并不存在 SaveToSessionRequestWrapper 类。封装的 SaveToSessionRequestWrapper 类主要作用是禁止在异步 Servlet 提交时,自动保存 SecurityContext。

为什么要禁止呢?我们来看如下一段简单的代码:

@GetMapping("/user2")
public void userInfo(HttpServletRequest req,HttpServletResponse resp){
    AsyncContext asyncContext=req.startAsync();
    CompletableFuture.runAsync(()->{
        try{
            PrintWriter out=asyncContext.getResponse().getWriter();
            out.write("hello javaboy!");
            asyncContext.complete();
        }catch(IOException e){
            e.printStackTrace();
        }
    });
}

可以看到,在异步 Servlet 中,当任务执行完毕之后, HttpServletResponse 也会自动提交,在提交的过程中会自动保存 SecurityContext 到 HttpSession 中,但是由于是在子线程中,因此无法获取到 SecurityContext 对象(SecurityContextHolder 默认将数据存储在 ThreadLocal 中),所以会保存失败。如果开发者使用了异步 Servlet,则默认情况下会禁用 HttpServletResponse 提交时自动保存 SecurityContext 这一功能,改为在 SecurityContextPersistenceFilter 过滤器中完成 SecurityContext 保存操作。

看完了 HttpSessionSecurityContextRepository 中封装的两个请求/响应对象之后,接下来我们再来整体上看一下 HttpSessionSecurityContextRepository 类的功能:

public class HttpSessionSecurityContextRepository implements
        SecurityContextRepository {
    public static final String SPRING_SECURITY_CONTEXT_KEY =
            "SPRING_SECURITY_CONTEXT";
    private final Object contextObject =
            SecurityContextHolder.createEmptyContext();
    private boolean allowSessionCreation = true;
    private boolean disableUrlRewriting = false;
    private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;
    private AuthenticationTrustResolver trustResolver =
            new AuthenticationTrustResolverImpl();

    public SecurityContext loadContext(
            HttpRequestResponseHolder requestResponseHolder) {
        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpServletResponse response = requestResponseHolder.getResponse();
        HttpSession httpSession = request.getSession(false);
        SecurityContext context = readSecurityContextFromSession(httpSession);
        if (context == null) {
            context = generateNewContext();
        }
        SaveToSessionResponseWrapper wrappedResponse =
                new SaveToSessionResponseWrapper(response,
                        request,
                        httpSession != null,
                        context);
        requestResponseHolder.setResponse(wrappedResponse);
        requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(
                request, wrappedResponse));
        return context;
    }

    public void saveContext(SecurityContext context,
                            HttpServletRequest request,
                            HttpServletResponse response) {
        SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils
                .getNativeResponse(response,
                        SaveContextOnUpdateOrErrorResponseWrapper.class);
        if (responseWrapper == null) {
            throw new IllegalStateException("");
        }
        if (!responseWrapper.isContextSaved()) {
            responseWrapper.saveContext(context);
        }
    }

    public boolean containsContext(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return false;
        }
        return session.getAttribute(springSecurityContextKey) != null;
    }

    private SecurityContext readSecurityContextFromSession(
            HttpSession httpSession) {
        if (httpSession == null) {
            return null;
        }
        Object contextFromSession =
                httpSession.getAttribute(springSecurityContextKey);
        if (contextFromSession == null) {
            return null;
        }
        if (!(contextFromSession instanceof SecurityContext)) {
            return null;
        }
        return (SecurityContext) contextFromSession;
    }

    protected SecurityContext generateNewContext() {
        return SecurityContextHolder.createEmptyContext();
    }

    public void setAllowSessionCreation(boolean allowSessionCreation) {
        this.allowSessionCreation = allowSessionCreation;
    }

    public void setDisableUrlRewriting(boolean disableUrlRewriting) {
        this.disableUrlRewriting = disableUrlRewriting;
    }

    public void setSpringSecurityContextKey(String springSecurityContextKey) {
        this.springSecurityContextKey = springSecurityContextKey;
    }

    private static class SaveToSessionRequestWrapper extends
            HttpServletRequestWrapper {
            //省略
    }

    final class SaveToSessionResponseWrapper extends
            SaveContextOnUpdateOrErrorResponseWrapper {
            //省略
    }

    private boolean isTransientAuthentication(Authentication authentication) {
        return AnnotationUtils.getAnnotation(authentication.getClass(),
                Transient.class) != null;
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }
}
  1. 首先通过 SPRING_SECURITY_CONTEXT_KEY 变量定义了 SecurityContext 在 HttpSession 中存储的 key,如果开发者需要手动操作 HttpSession 中存储的 SecurityContext,可以通过该 key 来操作。

  2. trustResolver 是一个用户身份评估器,用来判断当前用户是匿名用户还是通过 RememberMe 登录的用户。

  3. 在 loadContext 方法中,通过调用 readSecurityContextFromSession 方法来获取 SecurityContext 对象。如果获取到的对象为 null,则调用 generateNewContext 方法去生成一个空的 SecurityContext 对象,最后构造请求和响应的装饰类并存入 requestResponseHolder 对象中。

  4. saveContext 方法用来保存 SecurityContext,在保存之前,会先调用 isContextSaved 方法判断是否已经保存了,如果已经保存了,则不再保存。正常情况下,在 HttpServletResponse 提交时 SecurityContext 就已经保存到 HttpSession 中了;如果是异步 Servlet,则提交时不会自动将 SecurityContext 保存到 HttpSession,此时会在这里进行保存操作。

  5. containsContext 方法用来判断请求中是否存在 SecurityContext 对象。

  6. readSecurityContextFromSession 方法执行具体的 SecurityContext 读取逻辑,从 HttpSession 中获取 SecurityContext 并返回。

  7. generateNewContext 方法用来生成一个不包含 Authentication 的空的 SecurityContext 对象。

  8. setAllowSessionCreation 方法用来设置是否允许创建 HttpSession,默认是 true。

  9. setDisableUrlRewriting 方法表示是否禁用 URL 重写,默认是 false。

  10. setSpringSecurityContextKey 方法可以用来配置 HttpSession 中存储 SecurityContext 的 key。

  11. isTransientAuthentication 方法用来判断 Authentication 是否免于存储。

  12. setTrustResolver 方法用来配置身份评估器。

这就是 HttpSessionSecurityContextRepository 所提供的所有功能,这些功能都将在 SecurityContextPersistenceFilter 过滤器中进行调用,那么接下来我们就来看一下 SecurityContext PersistenceFilter 中的调用逻辑:

public class SecurityContextPersistenceFilter extends GenericFilterBean {
    private SecurityContextRepository repo;
    private boolean forceEagerSessionCreation = false;

    public SecurityContextPersistenceFilter() {
        this(new HttpSessionSecurityContextRepository());
    }

    public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
        this.repo = repo;
    }

    public void doFilter(ServletRequest req,
                         ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();
        }
        HttpRequestResponseHolder holder =
                new HttpRequestResponseHolder(request, response);
        SecurityContext contextBeforeChainExecution =
                repo.loadContext(holder);
        try {
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            chain.doFilter(holder.getRequest(), holder.getResponse());
        } finally {
            SecurityContext contextAfterChainExecution =
                    SecurityContextHolder.getContext();
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution,
                    holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);
        }
    }

    public void setForceEagerSessionCreation(
            boolean forceEagerSessionCreation) {
        this.forceEagerSessionCreation = forceEagerSessionCreation;
    }
}

过滤器的核心方法当然是 doFilter,我们就从 doFilter 方法开始介绍:

  1. 首先从 request 中获取 FILTER_APPLIED 属性,如果该属性值不为 null,则直接执行 chain.doFilter 方法,当前过滤器到此为止,这个判断主要是确保该请求只执行一次该过滤器。如果确实是该 request 第一次经过该过滤器,则给其设置上 FILTER_APPLIED 属性。

  2. forceEagerSessionCreation 变量表示是否要在过滤器链执行之前确保会话有效,由于这是一个比较耗费资源的操作,因此默认为 false。

  3. 构造 HttpRequestResponseHolder 对象,将 HttpServletRequest 和 HttpServletResponse 都存储进去。

  4. 调用 repo.loadContext 方法去加载 SecurityContext,repo 实际上就是我们前面所说 HttpSessionSecurityContextRepository 的实例,所以 loadContext 方法这里就不再赘述了。

  5. 将读取到的 SecurityContext 存入 SecurityContextHolder 之中,这样,在接下来的处理逻辑中,开发者就可以直接通过 SecurityContextHolder 获取当前登录用户对象了。

  6. 调用 chain.doFilter 方法使请求继续向下走,但是要注意,此时传递的 request 和 response 对象是在 HttpSessionSecurityContextRepository 中封装后的对象,即 SaveToSessionResponseWrapper 和 SaveToSessionRequestWrapper 的实例。

  7. 当请求处理完毕后, 在 finally 模块中,获取最新的 SecurityContext 对象(开发者可能在后续处理中修改了 SecurityContext 中的 Authentication 对象),然后清空 SecurityContextHolder 中的数据;再调用 repo.saveContext 方法保存 SecurityContext,具体的保存逻辑前面已经说过,这里就不再赘述了。

  8. 最后,从 request 中移除 FILTER_APPLIED 属性。

这就是整个 SecurityContextPersistenceFilter 过滤器的工作逻辑。一言以蔽之,请求在到达 SecurityContextPersistenceFilter 过滤器之后,先从 HttpSession 中读取 SecurityContext 出来,并存入 SecurityContextHolder 之中以备后续使用;当请求离开 SecurityContextPersistenceFilter 过滤器的时候,获取最新的 SecurityContext 并存入 HttpSession 中,同时清空 SecurityContextHolder 中的登录用户信息。

这就是第一种登录数据的获取方式,即从 SecurityContextHolder 中获取。

从当前请求对象中获取

接下来我们来看一下第二种登录数据获取方式——从当前请求中获取。获取代码如下:

@RequestMapping("/authentication")
public void authentication(Authentication authentication){
    System.out.println("authentication = "+authentication);
}

@RequestMapping("/principal")
public void principal(Principal principal){
    System.out.println("principal = "+principal);
}

开发者可以直接在 Controller 的请求参数中放入 Authentication 对象来获取登录用户信息。通过前面的讲解,大家已经知道 Authentication 是 Principal 的子类,所以也可以直接在请求参数中放入 Principal 来接收当前登录用户信息。需要注意的是,即使参数是 Principal,真正的实例依然是 Authentication 的实例。

用过 Spring MVC 的读者都知道,Controller 中方法的参数都是当前请求 HttpServletRequest 带来的。毫无疑问,前面的 Authentication 和 Principal 参数也都是 HttpServletRequest 带来的,那么这些数据到底是何时放入 HttpServletRequest 的呢?又是以何种形式存在的呢?接下来我们一起分析一下。

在 Servlet 规范中,最早有三个和安全管理相关的方法:

public String getRemoteUser();
public boolean isUserInRole(String role);
public java.security.Principal getUserPrincipal();
  1. getRemoteUser 方法用来获取登录用户名。

  2. isUserInRole 方法用来判断当前登录用户是否具备某一个指定的角色。

  3. getUserPrincipal 方法用来获取当前认证主体。

从 Servlet 3.0 开始,在这三个方法的基础之上,又增加了三个和安全管理相关的方法:

public boolean authenticate(HttpServletResponse response) throws IOException, ServletException;
public void login(String username, String password) throws ServletException;
public void logout() throws ServletException;
  1. authenticate 方法可以判断当前请求是否认证成功。

  2. login 方法可以执行登录操作。

  3. logout 方法可以执行注销操作。

不过 HttpServletRequest 只是一个接口,这些安全认证相关的方法,在不同环境下会有不同的实现。

如果是一个普通的 Web 项目,不使用任何框架, HttpServletRequest 的默认实现类是 Tomcat 中的 RequestFacade,从这个类的名字上就可以看出来,这是一个使用了 Facade 模式(外观模式)的类,真正提供底层服务的是 Tomcat 中的 Request 对象,只不过这个 Request 对象在实现 Servlet 规范的同时, 还定义了很多 Tomcat 内部的方法,为了避免开发者直接调用到这些内部方法,这里使用了外观模式。

在 Tomcat 的 Request 类中,对上面这些方法都做了实现,基本上都是基于 Tomcat 提供的 Realm 来实现的,这种认证方式非常冷门,项目中很少使用,因此这里不做过多介绍,感兴趣的读者可以查看 https://github.com/lenve/javaboy-code-samples 仓库中的 basiclogin 案例来了解其用法。

如果使用了 Spring Security 框架,那么我们在 Controller 参数中拿到的 HttpServletRequest 实例将是 Servlet3SecurityContextHolderAwareRequestWrapper,很明显,这是被 Spring Security 封装过的请求。

我们来看一下 Servlet3SecurityContextHolderAwareRequestWrapper 的继承关系,如图 2-21 所示。

image 2024 04 11 11 00 37 839
Figure 10. 图 2-21 Servlet3SecurityContextHolderAwareRequestWrapper 继承关系图

HttpServletRequestWrapper 就不用过多介绍了,SecurityContextHolderAwareRequest Wrapper 类主要实现了 Servlet 3.0 之前和安全管理相关的三个方法,也就是 getRemoteUser()、 isUserInRole(String) 以及 getUserPrincipal()。Servlet 3.0 中新增的三个安全管理相关的方法,则在 Servlet3SecurityContextHolderAwareRequestWrapper 类中实现。获取用户登录信息主要和前面三个方法有关,因此这里我们主要来看一下 SecurityContextHolderAwareRequestWrapper 类中相关方法的实现。

public class SecurityContextHolderAwareRequestWrapper
        extends HttpServletRequestWrapper {

    private final AuthenticationTrustResolver trustResolver;
    private final String rolePrefix;

    public SecurityContextHolderAwareRequestWrapper(
            HttpServletRequest request,
            String rolePrefix) {
        this(request, new AuthenticationTrustResolverImpl(), rolePrefix);
    }

    public SecurityContextHolderAwareRequestWrapper(
            HttpServletRequest request,
            AuthenticationTrustResolver trustResolver, String rolePrefix) {
        super(request);
        this.rolePrefix = rolePrefix;
        this.trustResolver = trustResolver;
    }

    private Authentication getAuthentication() {
        Authentication auth =
                SecurityContextHolder.getContext().getAuthentication();
        if (!trustResolver.isAnonymous(auth)) {
            return auth;
        }
        return null;
    }

    @Override
    public String getRemoteUser() {
        Authentication auth = getAuthentication();
        if ((auth == null) || (auth.getPrincipal() == null)) {
            return null;
        }
        if (auth.getPrincipal() instanceof UserDetails) {
            return ((UserDetails) auth.getPrincipal()).getUsername();
        }
        return auth.getPrincipal().toString();
    }

    @Override
    public Principal getUserPrincipal() {
        Authentication auth = getAuthentication();
        if ((auth == null) || (auth.getPrincipal() == null)) {
            return null;
        }
        return auth;
    }

    private boolean isGranted(String role) {
        Authentication auth = getAuthentication();
        if (rolePrefix != null && role != null
                && !role.startsWith(rolePrefix)) {
            role = rolePrefix + role;
        }
        if ((auth == null) || (auth.getPrincipal() == null)) {

            return false;
        }
        Collection<? extends GrantedAuthority> authorities =
                auth.getAuthorities();
        if (authorities == null) {
            return false;
        }
        for (GrantedAuthority grantedAuthority : authorities) {
            if (role.equals(grantedAuthority.getAuthority())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isUserInRole(String role) {
        return isGranted(role);
    }
}

SecurityContextHolderAwareRequestWrapper 类其实非常好理解:

  1. getAuthentication:该方法用来获取当前登录对象 Authentication,获取方式就是我们前面所讲的从 SecurityContextHolder 中获取。如果不是匿名对象就返回,否则就返回 null。

  2. getRemoteUser:该方法返回了当前登录用户的用户名,如果 Authentication 对象中存储的 Principal 是当前登录用户对象,则返回用户名;如果 Authentication 对象中存储的 Principal 是当前登录用户名(字符串),则直接返回即可。

  3. getUserPrincipal:该方法返回当前登录用户对象,其实就是 Authentication 的实例。

  4. isGranted:该方法是一个私有方法,作用是判断当前登录用户是否具备某一个指定的角色。判断逻辑也很简单,先对传入进来的角色进行预处理,有的情况下可能需要添加 ROLE_ 前缀,角色前缀的问题在本书后面的章节中会做详细介绍,这里先不做过多的展开。然后调用 Authentication#getAuthorities 方法,获取当前登录用户所具备的所有角色,最后再和传入进来的参数进行比较。

  5. isUserInRole:该方法调用 isGranted 方法,进而实现判断当前用户是否具备某一个指定角色的功能。

看到这里,相信读者已经明白了,在使用了 Spring Security 之后,我们通过 HttpServletRequest 就可以获取到很多当前登录用户信息了,代码如下:

@RequestMapping("/info")
public void info(HttpServletRequest req) {
    String remoteUser = req.getRemoteUser();
    Authentication auth = ((Authentication) req.getUserPrincipal());
    boolean admin = req.isUserInRole("admin");
    System.out.println("remoteUser = " + remoteUser);
    System.out.println("auth.getName() = " + auth.getName());
    System.out.println("admin = " + admin);
}

执行该方法,打印结果如下:

remoteUser = javaboy
auth.getName() = javaboy
admin = false

前面我们直接将 Authentication 或者 Principal 写到 Controller 参数中,实际上就是 Spring MVC 框架从 Servlet3SecurityContextHolderAwareRequestWrapper 中提取的用户信息。

那么 Spring Security 是如何将默认的请求对象转化为 Servlet3SecurityContextHolderAwareRequestWrapper 的呢?这就涉及 Spring Security 过滤器链中另外一个重要的过滤器——SecurityContextHolderAwareRequestFilter。

前面我们提到 Spring Security 过滤器中,有一个 SecurityContextHolderAwareRequest Filter 过滤器,该过滤器的主要作用就是对 HttpServletRequest 请求进行再包装,重写 HttpServletRequest 中和安全管理相关的方法。 HttpServletRequest 在整个请求过程中会被包装多次,每一次的包装都会给它增添新的功能,例如在经过 SecurityContextPersistenceFilter 请求时就会对它进行包装。

我们来看一下 SecurityContextHolderAwareRequestFilter 过滤器的源码(部分):

public class SecurityContextHolderAwareRequestFilter
        extends GenericFilterBean {
    public void doFilter(ServletRequest req,
                         ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
                (HttpServletResponse) res), res);
    }

    private HttpServletRequestFactory createServlet3Factory(String rolePrefix) {
        HttpServlet3RequestFactory factory =
                new HttpServlet3RequestFactory(rolePrefix);
        factory.setTrustResolver(this.trustResolver);
        factory.setAuthenticationEntryPoint(this.authenticationEntryPoint);
        factory.setAuthenticationManager(this.authenticationManager);
        factory.setLogoutHandlers(this.logoutHandlers);
        return factory;
    }
}

final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
    @Override
    public HttpServletRequest create(HttpServletRequest request,
                                     HttpServletResponse response) {
        return new Servlet3SecurityContextHolderAwareRequestWrapper(request,
                this.rolePrefix, response);
    }

    private class Servlet3SecurityContextHolderAwareRequestWrapper
            extends SecurityContextHolderAwareRequestWrapper {
        //......
    }
}

从这段源码中可以看到,在 SecurityContextHolderAwareRequestFilter#doFilter 方法中,会调用 requestFactory.create 方法对请求重新进行包装。 requestFactory 就是 HttpServletRequestFactory 类的实例,它的 create 方法里边就直接创建了一个 Servlet3SecurityContextHolderAwareRequestWrapper 实例。

对请求的 HttpServletRequest 包装之后,接下来在过滤器链中传递的 HttpServletRequest 对象,它的 getRemoteUser()、 isUserInRole(String) 以及 getUserPrincipal()方法就可以直接使用了。

HttpServletRequest 中 getUserPrincipal() 方法有了返回值之后,最终在 Spring MVC 的 ServletRequestMethodArgumentResolver#resolveArgument(Class<?>, HttpServletRequest) 方法中进行默认参数解析,自动解析出 Principal 对象。开发者在 Controller 中既可以通过 Principal 来接收参数,也可以通过 Authentication 对象来接收。

经过前面的介绍,相信读者对于 Spring Security 中两种获取登录用户信息的方式,以及这两种获取方式的原理,都有一定的了解了。