Spring Security登录后用户数据的获取

928 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

Spring Security登录后用户数据的获取

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

(1)从 SecurityContextHolder 中获取。
(2)从当前请求对象中获取。

\quad这里列出来的两种方式是常见获取登录成功后的用户信息,例如直接从 HttpSession 中获取用户登录数据。 无论是哪种获取方式,都离不开一个重要的对象:Authentication。在Spring Security 中, Authentication 对象主要有两方面的功能:

(1)作为 AuthenticationManager 的输入参数,提供用户身份认证的凭证,当它作为一个 输入参数时,它的 isAuthenticated 方法返回 false,表示用户还未认证。
(2)代表已经经过身份认证的用户,此时的 Authentication 可以从 SecurityContext 中获取。

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

(1)principal:定义认证的用户。如果用户使用用户名/密码的方式登录,principal 通常 就是一个 UserDetails 对象。
(2)credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动 擦除,以防止泄漏。
(3)authorities:用户被授予的权限信息。

\quadJava的 java.security 包中本身提供了 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) throws IllegalArgumentException;

}

\quad该接口中定义的方法的功能如下:

  • getAuthorities 方法:用来获取用户权限。
  • getCredentials 方法:用来获取用户凭证,一般来说就是密码。
  • getDetails 方法:用来获取用户的详细信息,可能是当前的请求之类。
  • getPrincipal 方法:用来获取当前用户信息,可能是一个用户名,也可能是一个用户对象。
  • isAuthenticated 方法:当前用户是否认证成功。

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

1. 从SecurityContextHolder中获取

\quadAuthentication 是用来在保存用户的信息,那么获取的用户的信息是用SecurityContextHolder这个类来进行处理的。SecurityContextHolder 中存储 的 是 SecurityContext , SecurityContext 中 存储的则是 Authentication,三者的关系如图所示,再举个使用从SecurityContextHolder中获取信息的例子:

image.png

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);
    }
}

\quad配置完成后,启动项目,登录成功后,访问/user 接口,控制台就会打印出登录用户信息。 \quad从SecurityContextHolder的源码(源码太长,就不贴了)中可以看到,SecurityContextHolder 定义了三个静态常量用来描述三种不同的存储策略;存储策略 strategy 会在静态代码块中进行初始化,根据不同的 strategyName 初始化不同的存储策略;strategyName 变量表示目前正在使用的存储策略,开发者可以通过配置系统变量或者调用 setStrategyName 来修改 SecurityContextHolder中的存储策略 , 调用 setStrategyName 后会重新初始化 strategy。

  1. MODE_THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,而ThreadLocal 的特点是只能在当前线程获取信息,这也就非常适合 Web 应用,因为在默认情况下,一个请求都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着在子线程中去获取登录用户数据,就会获取不到。
  2. MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  3. MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在 Java Web 开发中,这种模式很少使用到。

2.从当前请求对象中获取

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

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

\quad一般可以直接在 Controller 的请求参数中放入 Authentication 对象来获取登录用户信息。 通过前面的讲解,大家已经知道 Authentication 是 Principal 的子类,所以也可以直接在请求参数中放入 Principal 来接收当前登录用户信息。需要注意的是,即使参数是 Principal,真正的实例依然是 Authentication 的实例。但是这样写我们会感觉很奇怪,其实 Controller 中方法的参数都是当前请求 HttpServletRequest 带来的。也就是说,Authentication 和 Principal 参数也都是 HttpServletRequest 带来的。
\quad因为我们使用了 Spring Security 框架,那么我们在 Controller 参数中拿到的 HttpServletRequest 实例其实是 Servlet3SecurityContextHolderAwareRequestWrapper,很明显,这是被 Spring Security 封装过的请求。
\quadSecurityContextHolderAwareRequestWrapper 类其实非常好理解:

  1. getAuthentication:该方法用来获取当前登录对象 Authentication,获取方式就是我们前面所讲的从 SecurityContextHolder 中获取。如果不是匿名对象就返回,否则就返回 null。
  2. getRemoteUser:该方法返回了当前登录用户的用户名,如果 Authentication 对象中存储的 Principal 是当前登录用户对象,则返回用户名;如果 Authentication 对象中存储的 Principal 是当前登录用户名(字符串),则直接返回即可。
  3. getUserPrincipal:该方法返回当前登录用户对象,其实就是 Authentication 的实例。
  4. isGranted:该方法是一个私有方法,作用是判断当前登录用户是否具备某一个指定的角色。判断逻辑也很简单,先对传入进来的角色进行预处理,有的情况下可能需要添加 ROLE_ 前缀,然后调用 Authentication里的 getAuthorities 方法,获取当前登录用户所具备的所有角色,最后再和传入进来的参数进行比较。
  5. isUserInRole:该方法调用 isGranted 方法,判断当前用户是否具备某一个指定角色的功能。

\quad了解完这个类之后,我们在使用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);
}

\quad前面我们直接将 Authentication 或者 Principal 写到 Controller 参数中,实际上就是 Spring MVC 框架从 Servlet3SecurityContextHolderAwareRequestWrapper 中提取的用户信息,其实是在SecurityContextHolderAwareRequestFilter 过滤器处理的。
\quad在 SecurityContextHolderAwareRequestFilter#doFilter 方法中,会调用 requestFactory.create 方法对请求重新进行包装。requestFactory 就是 HttpServletRequestFactory 类的实例,它的 create 方法里边就直接创建了一个 Servlet3SecurityContextHolderAware RequestWrapper 实例。 对请求的 HttpServletRequest 包装之后,接下来在过滤器链中传递的 HttpServletRequest 对象,它的 getRemoteUser()、isUserInRole(String)以及 getUserPrincipal()方法就可以直接使用了。
\quadHttpServletRequest 中 getUserPrincipal()方法有了返回值之后,最终在 Spring MVC 的 ServletRequestMethodArgumentResolver#resolveArgument(Class, HttpServletRequest)方法中进行默认参数解析,自动解析出 Principal 对象。开发者在 Controller 中既可以通过 Principal 来接收参数,也可以通过 Authentication 对象来接收。