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

原文链接:http://leozzy.com/?p=119

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 authorities = user.getAuthorities();
    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, newPwd, authorities));
}

你可能感兴趣的:(Spring Boot 如何通过 Security Context 缓存账号密码)