英文:
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(() -> 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;
}
}
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 && (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)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论