diff --git a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java index d6a5b7c8878..291f15f3223 100644 --- a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java @@ -30,6 +30,7 @@ import org.springframework.core.log.LogMessage; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -156,6 +157,10 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsMa private UserCache userCache = new NullUserCache(); + private RowMapper userDetailsMapper = this::mapToUser; + + private RowMapper grantedAuthorityMapper = this::mapToGrantedAuthority; + public JdbcUserDetailsManager() { } @@ -163,6 +168,37 @@ public JdbcUserDetailsManager(DataSource dataSource) { setDataSource(dataSource); } + /** + * Sets the {@code RowMapper} to convert each user result row into a + * {@link UserDetails} object. + * + * The default mapper expects columns with names like 'username', 'password', + * 'enabled', etc., and maps them directly to the corresponding UserDetails + * properties. + * @param mapper the {@code RowMapper} to use for mapping rows in the database, must + * not be null + * @since 6.5 + */ + public void setUserDetailsMapper(RowMapper mapper) { + Assert.notNull(mapper, "userDetailsMapper cannot be null"); + this.userDetailsMapper = mapper; + } + + /** + * Sets the {@code RowMapper} to convert each authority result row into a + * {@link GrantedAuthority} object. + * + * The default mapper expects columns with names like 'authority' or 'role', and maps + * them directly to SimpleGrantedAuthority objects. + * @param mapper the {@code RowMapper} to use for mapping rows in the database to + * GrantedAuthority objects, must not be null + * @since 6.5 + */ + public void setGrantedAuthorityMapper(RowMapper mapper) { + Assert.notNull(mapper, "grantedAuthorityMapper cannot be null"); + this.grantedAuthorityMapper = mapper; + } + @Override protected void initDao() throws ApplicationContextException { if (this.authenticationManager == null) { @@ -178,7 +214,7 @@ protected void initDao() throws ApplicationContextException { */ @Override protected List loadUsersByUsername(String username) { - return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username); + return getJdbcTemplate().query(getUsersByUsernameQuery(), this.userDetailsMapper, username); } private UserDetails mapToUser(ResultSet rs, int rowNum) throws SQLException { @@ -387,7 +423,7 @@ public List findGroupAuthorities(String groupName) { this.logger.debug("Loading authorities for group '" + groupName + "'"); Assert.hasText(groupName, "groupName should have text"); return getJdbcTemplate().query(this.groupAuthoritiesSql, new String[] { groupName }, - this::mapToGrantedAuthority); + this.grantedAuthorityMapper); } private GrantedAuthority mapToGrantedAuthority(ResultSet rs, int rowNum) throws SQLException { diff --git a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java index 67d0c83fa59..65b1d4b1c45 100644 --- a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java +++ b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java @@ -16,6 +16,7 @@ package org.springframework.security.provisioning; +import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; import org.springframework.security.PopulatedDatabase; import org.springframework.security.TestDataSource; import org.springframework.security.access.AccessDeniedException; @@ -48,14 +50,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; /** * Tests for {@link JdbcUserDetailsManager} * * @author Luke Taylor + * @author dae won */ public class JdbcUserDetailsManagerTests { @@ -365,6 +369,42 @@ public void createNewAuthenticationUsesNullPasswordToKeepPassordsSave() { assertThat(updatedAuth.getCredentials()).isNull(); } + @Test + public void setUserDetailsMapperWithNullMapperThrowsException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.manager.setUserDetailsMapper(null)) + .withMessage("userDetailsMapper cannot be null"); + } + + @Test + public void setUserDetailsMapperWithMockMapper() throws SQLException { + RowMapper mockMapper = mock(RowMapper.class); + given(mockMapper.mapRow(any(), anyInt())).willReturn(joe); + this.manager.setUserDetailsMapper(mockMapper); + insertJoe(); + UserDetails newJoe = this.manager.loadUserByUsername("joe"); + assertThat(joe).isEqualTo(newJoe); + verify(mockMapper).mapRow(any(), anyInt()); + } + + @Test + public void setGrantedAuthorityMapperWithNullMapperThrowsException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.manager.setGrantedAuthorityMapper(null)) + .withMessage("grantedAuthorityMapper cannot be null"); + } + + @Test + public void setGrantedAuthorityMapperWithMockMapper() throws SQLException { + RowMapper mockMapper = mock(RowMapper.class); + GrantedAuthority mockAuthority = new SimpleGrantedAuthority("ROLE_MOCK"); + given(mockMapper.mapRow(any(), anyInt())).willReturn(mockAuthority); + this.manager.setGrantedAuthorityMapper(mockMapper); + List authGroup = this.manager.findGroupAuthorities("GROUP_0"); + assertThat(authGroup.get(0)).isEqualTo(mockAuthority); + verify(mockMapper).mapRow(any(), anyInt()); + } + private Authentication authenticateJoe() { UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.authenticated("joe", "password", joe.getAuthorities());