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

huangapple go评论76阅读模式
英文:

Caching UserDetails using Springboot

问题

以下是您提供的代码段的翻译部分:

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("The user " + username + " cannot be found!"));

        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

这个警告在其意义上很清楚,因为密码为空,它无法对用户进行身份验证。但我无法理解为什么从缓存中获取的用户的密码为空,即使它已经被正确存储。我不确定如何在使用缓存的情况下解决这个问题。您有任何想法吗?

非常感谢您的帮助!

英文:

I am trying to implement a UserCache in my application to avoid to make multiple calls to the User table in the case I am using the basic authentication. I created my CacheConfig following the accepted answer of this topic, in which the CachingUserDetailsService is used to manage the user cache. Bellow is the code of the UserService, CacheConfig and SecurityConfig:


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(() -&gt; new UsernameNotFoundException(&quot;O usu&#225;rio &quot; + username + &quot; n&#227;o pode ser encontrado!&quot;));

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


        return userDetails;
    }

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


}

@EnableCaching
@Configuration
public class CacheConfig {

    public static final String USER_CACHE = &quot;userCache&quot;;

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

        //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(&quot;userCache&quot;);
        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 -&gt; new CorsConfiguration().applyPermitDefaultValues())
                .and()
                .authorizeRequests()
                    .antMatchers(&quot;/api/**&quot;)
                        .authenticated()
                .and()
                    .httpBasic();
    }

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

}

The first call works well because it makes the call to the UserRepository. But on the second, it does not make the call to the repository (as expected) but I am getting the following WARN from BCryptPasswordEncoder:

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

The warning is clear in its meaning and it fails to authenticate de user because of the null password. But I cannot understand why the user retried from cache has a null password if it was correctly stored. I am not sure how to solve it using the cache. Any thoughts?

Thank you very much for your help!

答案1

得分: 2

@M.Deinum的评论是完全正确的。您可以在这里参考文档1

需要注意的是,这个实现不是不可变的。它实现了CredentialsContainer接口,以便在认证后允许擦除密码。如果您在内存中存储实例并重用它们,这可能会引起副作用。如果是这样,请确保您每次调用UserDetailsService时都返回一个副本。

如果您有兴趣,可以查看Spring Security的源代码

private boolean eraseCredentialsAfterAuthentication = true;

...

if (result != null) {
    if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
        // 认证完成。从认证中删除凭据和其他机密数据
        ((CredentialsContainer) result).eraseCredentials();
    }
    // 如果尝试并成功的父AuthenticationManager,它将发布一个AuthenticationSuccessEvent
    // 此检查防止父AuthenticationManager已经发布了重复的AuthenticationSuccessEvent
    if (parentResult == null) {
        this.eventPublisher.publishAuthenticationSuccess(result);
    }

    return result;
}

以及User.java的源代码:

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

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

如果您确实需要这样做,您可以根据文档中的说明将默认标志更改为false,只需注入AuthenticationManager并调用:

setEraseCredentialsAfterAuthentication(false)
英文:

@M.Deinum comment is absolutely correct. You can refer to the doc 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.

You can check the Spring-security source code if you are curious more:

private boolean eraseCredentialsAfterAuthentication = true;
    
...

if (result != null) {
	if (this.eraseCredentialsAfterAuthentication &amp;&amp; (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;
}

And User.java source code:

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

By the way, it looks weird to cache login user that way. During login, it is better to get fresh record from DB instead of from cache. You can use cached user at other place but seldom see it is used during login.

If you really need to do that, you can change the default flag to false as mentioned in doc, just inject AuthenticationManager and call:

setEraseCredentialsAfterAuthentication(false)

huangapple
  • 本文由 发表于 2020年9月24日 19:56:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/64045929.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定