Please modify the method and timing of setting basename in this class. Because calling setBasename() will cause basenameSet to be cleared. If the user uses his own message source, Spring Security will not be able to localize the message.
public class SpringSecurityMessageSource extends ResourceBundleMessageSource {
public SpringSecurityMessageSource() {
setBasename("org.springframework.security.messages");
}
public static MessageSourceAccessor getAccessor() {
return new MessageSourceAccessor(new SpringSecurityMessageSource());
}
}
public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {
private final Set<String> basenameSet = new LinkedHashSet<>(4);
...
public void setBasename(String basename) {
setBasenames(basename);
}
public void setBasenames(String... basenames) {
this.basenameSet.clear();
addBasenames(basenames);
}
...
}
Comment From: marcusdacoregio
Hi, @jacko9et. I don't think I follow exactly where the problem is. Can you provide a minimal, reproducible sample?
Comment From: jacko9et
If the DaoAuthenticationProvider is custom, i18n will not work. Only two classes are needed to reproduce it.
SecurityConfig
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, ObjectMapper objectMapper) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
.failureHandler((request, response, exception) -> {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try (PrintWriter writer = response.getWriter()) {
writer.print(objectMapper.writeValueAsString(
Map.of("message", exception.getLocalizedMessage())));
writer.flush();
}
})
);
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
UserDetailsService users(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("admin")
.password(passwordEncoder.encode("admin"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
CustomizedAuthenticationProvider
@Component
public class CustomizedAuthenticationProvider extends DaoAuthenticationProvider {
public CustomizedAuthenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
setUserDetailsService(userDetailsService);
setPasswordEncoder(passwordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
Comment From: marcusdacoregio
Can you please elaborate more? What is the current and expected behavior? In addition to those 2 classes, what are the steps that we need to perform to reproduce it? What should we observe?
Comment From: jacko9et
@marcusdacoregio Taking the Chrome browser as an example, first we need to add non-English languages to the browser. 1. Settings -> Languages -> Add Languages -> French (France) , and then Move to the top. 2. Only use the SecurityConfig configuration class. 3. Enter wrong username and password to login, it should return a message like this.
{
"message": "Les identifications sont erronées"
}
- Add a second Spring Bean, CustomizedAuthenticationProvider class.
- Login again using the wrong username and password. Its return value is like this.
{
"message": "Bad credentials"
}
This means that Spring Security's i18n will not work if you add a custom DaoAuthenticationProvider class.
Comment From: marcusdacoregio
The DaoAuthenticationProvider implements the MessageSourceAware, which means that when you expose it as a Spring bean (via @Component on CustomizedAuthenticationProvider) Spring will inject the messageSource bean into it.
Since your application does not have that bean, it will inject an empty message source.
To work around that, I can suggest a couple of options:
- Override
setMessageSourceand make it a no-op, this will not override the default:
@Component
public class CustomizedAuthenticationProvider extends DaoAuthenticationProvider {
// ...
@Override
public void setMessageSource(MessageSource messageSource) {
// no-op
}
}
- Override
setMessageSourceand make it useSpringSecurityMessageSource:
@Component
public class CustomizedAuthenticationProvider extends DaoAuthenticationProvider {
// ...
@Override
public void setMessageSource(MessageSource messageSource) {
super.setMessageSource(new SpringSecurityMessageSource());
}
}
I like option 2 because you are explicit about the source you want to use. That said, I do not think this is a bug in Spring Security, therefore I'm closing this as solved. Thanks!
Comment From: jacko9et
@marcusdacoregio In fact, I did solve it like this, but what I want to say is that most custom DaoAuthenticationProvider just want to extend it rather than change its default implementation. It is also in the Spring Security context anyway, and I would prefer Spring Security to automatically perform such modifications on my behalf.
@Override
public void setMessageSource(MessageSource messageSource) {
if (messageSource instanceof ResourceBundleMessageSource bundleMessageSource) {
bundleMessageSource.getBasenameSet().addAll(new SpringSecurityMessageSource().getBasenameSet());
super.setMessageSource(messageSource);
}
}