Spring Boot 如何通过 Security Context 缓存账号密码

Spring Boot 如何通过 Security Context 缓存账号密码

SecurityContextHolder 是用来保存 SecurityContext 的,通过 SecurityContextHolder.getContext() 静态方法可以获得当前 SecurityContext 对象。

SecurityContext 持有代表当前用户相关信息的 Authentication 的引用, Authentication 通过 SecurityContext 对象的 getAuthentication() 方法获得。

通过 Authentication.getPrincipal() 可以获取到代表当前用户的信息,这个对象通常是 UserDetails 的实例。

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

获取到的 UserDetail 中包含用户名和密码等用户信息,但是该密码是经过 Spring 加密的并且不可逆(hash + salt)的。

那么我们如何才能拿到明文的密码呢?

每次登录时,表单中填写用户名密码,以这里作为切入点,找到登陆接口以及 Spring Security 对表单中的账号密码进行认证的地方,那就是自己定义的 AuthenticationProvider 的实现类,authenticate() 方法参数中有 Authentication 对象,这是 Spring 自己已经封装好的对象,其中包含账号密码信息。

String username = authentication.getName();
String password = (String) authentication.getCredentials();

但这个 authenticate() 是由 SpringSecurity 来调用的,我们无法在其他方法中调用这个方法获取账号密码。那么就直接模仿该方法,通过 SecurityContextHolder.getContext().getAuthentication() 获得 Authentication 对象,而不是文章开头那样拿到 UserDetail 对象(包含的是加密后的密码),再通过 getCredentials 即可获得明文密码。

就这么简单吗?通过调试发现除了在 AuthenticationProvider 实现类的 authenticate() 认证方法中能够通过这种方式获得明文密码,其他地方使用 Authentication 拿到的密码都是 null,原因如下:

默认情况下,在认证成功后,ProviderManager 会清除返回的 Authentication 中的凭证信息,如密码。所以如果你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么以后你再利用缓存的信息去认证将会失败,因为它已经不存在密码这样的凭证信息了。所以在使用缓存的时候你应该考虑到这个问题。一种解决办法是设置 ProviderManager 的 eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一起缓存。

那么关键是如何设置 eraseCredentialsAfterAuthentication 属性呢?

在继承了 WebSecurityConfigurerAdapter 的类中,重写 configure(AuthenticationManagerBuilder auth) 方法,设置 ProviderManager 的属性即可。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.eraseCredentials(false);
}

在页面退出登录时,会通过 clearAuthentication(true) 方法清空 SecurityContext Authentication 相关信息,以不同账号登录,保存的都是当时登录的 Authentication 信息。

如果在页面修改密码,那么 Authentication 默认不会更新,需要自己手动更新 SecurityContext 中 Authentication 的信息。如果不退出登录,使用了 Authentication 的地方依然使用的旧密码。

private void updateSecurityContext(String newPwd) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String username = authentication.getName();
    UserDetails user = kylinUserService.loadUserByUsername(username);
    Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, newPwd, authorities));
}
i

发表评论

电子邮件地址不会被公开。 必填项已用*标注