使用 Spring Boot 缓存用户详细信息

Caching UserDetails using Springboot

我正在尝试在我的应用程序中实现 UserCache,以避免在我使用基本身份验证的情况下多次调用用户 table。我根据此 的已接受答案创建了我的 CacheConfig,其中 CachingUserDetailsService 用于管理用户缓存。下面是 UserServiceCacheConfigSecurityConfig:

的代码

public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    public UserService(UserRepository repository) {
        this.userRepository = repository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        AddInUser user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("O usuário " + username + " não pode ser encontrado!"));

        UserDetails userDetails = User
                .builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .roles("USER")
                .build();


        return userDetails;
    }

    @Transactional
    public AddInUser save(AddInUser user) {
        return userRepository.save(user);
    }


}

@EnableCaching
@Configuration
public class CacheConfig {

    public static final String USER_CACHE = "userCache";

    /**
     * Define cache strategy
     *
     * @return CacheManager
     */
    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        List<Cache> caches = new ArrayList<>();

        //Failure after 5 minutes of caching
        caches.add(new GuavaCache(CacheConfig.USER_CACHE,
                CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build()));

        simpleCacheManager.setCaches(caches);

        return simpleCacheManager;
    }

    @Bean
    public UserCache userCache() throws Exception {
        Cache cache = cacheManager().getCache("userCache");
        return new SpringCacheBasedUserCache(cache);
    }


}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    };

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserCache userCache;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
                .and()
                .authorizeRequests()
                    .antMatchers("/api/**")
                        .authenticated()
                .and()
                    .httpBasic();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        UserService userService = new UserService(userRepository);
        CachingUserDetailsService cachingUserService = new CachingUserDetailsService(userService);
        cachingUserService.setUserCache(this.userCache);
        return cachingUserService;
    }

}

第一个调用效果很好,因为它调用了 UserRepository。但在第二个,它没有调用存储库(如预期的那样)但我从 BCryptPasswordEncoder:

收到以下警告
2020-09-24 08:43:51.327  WARN 24624 --- [nio-8081-exec-4] o.s.s.c.bcrypt.BCryptPasswordEncoder     : Empty encoded password

警告的含义很明确,由于 null 密码,它无法对用户进行身份验证。但是我不明白为什么从缓存中重试的用户在正确存储的情况下密码为空。我不确定如何使用缓存解决它。有什么想法吗?

非常感谢您的帮助!

@M.Deinum 评论完全正确。可以参考文档 here.

Note that this implementation is not immutable. It implements the CredentialsContainer interface, in order to allow the password to be erased after authentication. This may cause side-effects if you are storing instances in-memory and reusing them. If so, make sure you return a copy from your UserDetailsService each time it is invoked.

更多好奇可以查看Spring-security source code:

private boolean eraseCredentialsAfterAuthentication = true;
    
...

if (result != null) {
    if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
    }
    // If the parent AuthenticationManager was attempted and successful then it
    // will publish an AuthenticationSuccessEvent
    // This check prevents a duplicate AuthenticationSuccessEvent if the parent
    // AuthenticationManager already published it
    if (parentResult == null) {
        this.eventPublisher.publishAuthenticationSuccess(result);
    }

    return result;
}

User.java源代码:

@Override
public void eraseCredentials() {
    this.password = null;
}

顺便说一句,以这种方式缓存登录用户看起来很奇怪。在登录期间,最好从数据库而不是缓存中获取新记录。您可以在其他地方使用缓存的用户,但很少看到它在登录时被使用。

如果你真的需要这样做,你可以将默认标志更改为 false,如 doc 中所述,只需注入 AuthenticationManager 并调用:

setEraseCredentialsAfterAuthentication(false)