[SpringSecurity]]Controller该如何获取当前登录用户信息

6,915 阅读5分钟

1、 问题

在这次项目代码Review的过程中,我发现不少同事通喜欢在Contoller层甚至Service层直接操作SecurityContextHolder去获取当前登录的用户信息即Authentication对象。

    public static JwtUser getUserFromContext() {
        Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
        return details instanceof JwtUser ? (JwtUser) details : null;
    }

在设计上我们要努力避免Service层有外部资源,尤其是Web资源的依赖,这样对单元测试的代码非常的不友好。所以我更推荐在Controller层去与Web相关的资源去打交道,如果要用SecurityContextHolder至少也得在Controller层去操作。

在讨论如何在Controller层操作Authention的时候,我们先来讨论以下几个问题:

  • Authentication是什么
  • Authentication是什么时候如何被存储到SecurityContext中的
  • 我们的用户对象被存储在了Authentication的哪一个部分
  • 如何在Controller注入我们需要的信息

最后我们在讨论如何在Spring的Controller中获取当前的登录的Authentication对象。

2、 Authentication是什么?

Authentication是认证信息的载体,在Spring Security中,我们在尝试登陆向服务器发送请求时候包含的用户名和密码是Authentication,在这种场景下,我们更多见到的是带后缀Token的形式;当我们登陆成功之后,用于在上下文中标识我们具体身份信息的还是Authentication。 我们对AbstractAuthenticationToken进一步的展开进行讨论,AbstractAuthenticationToken实现了两个主要的接口,用于保存身份主体信息的Principal和Authentication一支,用于保存认证凭证比如密码的CredentialsContainer。通常生命周期在提交验证的那一刹那就结束了。验证完毕后,存储至``SecurityContext通常是不包含验证敏感信息的Authentication`形式。

登陆验证使用的载体也是一种Authentication

3、 Authentication是什么时候如何被存储到SecurityContext中的

Authentication是在验证成功后被设置到SecurityContext中的,AbstractAuthenticationProcessingFilter中的代码显示了这样一个操作:

		HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

通过源代码我们还发现,其实在验证登录成功后不仅会设置Context还会通过Event机制发送一个InteractiveAuthenticationSuccessEvent,我们通常也可以设置一个Event的Listener来处理一些登录验证后需要处理的业务逻辑,不至于去扩展AuthenticationFilter

4、 我们的用户对象被存储在了Authentication的哪一个部分

在搞明白了“身份验证成功后,Authentication信息将被存储至SecurityContext”这一个前提后,我们来解决Authentication中将存储哪些信息。 最常见的场景是我们将UserDetails信息存储至了Authentication的details字段中。

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
        \\ ...省略

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

如果你是自定义扩展的AuthenticationFilter也可以在attemptAuthentication方法中自定义如何组织Authentication的结构,但通常我们会将details字段用于放置UserDetails信息。根据验证场景的不同也可以放置任何后续在业务代码可能会使用的身份标志信息,比如IP,比如验证码等。 到这里我们应该能明白了验证成功后的整个Authentication的生命周期与结构了。

5、 如何在Controller注入我们需要的信息

在Controller中我们可能需要获取的当前验证登录的用户信息,最常见的例子是我们需要获取当前登录的用户名信息。

注入Principal获取登录时候的用户名

假设我们提交登录的时候用户名使用是用户名与密码,那么我们AuthenticationToken的Principal中存储的是用户名字段,在默认逻辑下,登录验证后的Authentication字段会与登录请求时候保持一致,那么Authentication中的Principal便是登录的用户名。下面一段Controller的代码便是通过Spring的注入机制获取上下文中的Principal对象。

    public UserModel findUser(Principal principal)  {
        UserModel userModel = userService.findOneByUsername(principal.getName());
        return userModel;
    }

注入UserDetails获取数据中的UserDetails特有信息

这种场景通常是因为登录时候使用的的登录标识信息与我们需要使用的信息不一致,最常见的场景是,系统支持使用手机号和用户名进行登录,但是数据库中的查询逻辑我们只想支持用户表中的用户名。如果从Principal获取,我们无法判断存储的是手机号还是用户名。那么我们便可以通过注入UserDetails对象,来获取Authentication的details字段中UserDetails信息。

    public UserModel findUser(UserDetails userDetails)  {
        UserModel userModel = userService.findOneByUsername(userDetails.getUsername());
        return userModel;
    }

注入Authentication获取更多的信息

如果Principal和UserDetails中的用户身份信息都不足以满足当前业务使用从场景,比如你需要验证当时存储在details的一些自定义结构信息,那么我们可以通过在Controller层注入Authentication直接操作当前上下文的中Authentication对象。 当我们明白了如何在Controller中操作Authentication之后,我们为了进行偷懒也可以ControllerAdvice中对Authentication进行拦截转型,将明确类型的User实现类实例放置于上下中,以便Controller更容易的进行注入:

@ControllerAdvice
public class CurrentUserAdvice {
    @ModelAttribute()
    public JwtUser currentUser(Authentication authentication) {
        JwtUser jwtUser = null;
        if(authentication!=null) {
             jwtUser = (JwtUser)authentication.getDetails();
        }
        return jwtUser;
    }
}

那么我们在Controller只需要通过@ModelAttribute去注入。

    public UserModel findUser(@ModelAttribute() JwtUser JwtUser)  {
        UserModel userModel = userService.findOneById(JwtUser.getId());
        return userModel;
    }

总结

SecurityContext的上下文实现方式其实也有很多,有依托于单机线程池也有依托于HttpSession的。在我们针对不同的应用部署场景时候也需要对应选择适合的策略。