阅前提示
此文章基于Spring Security 6.0
UserDetailsService
在之前的认证流程篇章中已经介绍到,DaoAuthenticationProvider使用了一个UserDetailsService去接收username,password以及一些其他的属性去做认证。然后Spring Security提供了in-memory内存 and JDBC 数据库 implementations of UserDetailsService。本篇章讲讲基于内存的UserDetails管理
InMemoryUserDetailsManager
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
protected final Log logger = LogFactory.getLog(getClass());
private final Map<String, MutableUserDetails> users = new HashMap<>();
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private AuthenticationManager authenticationManager;
public InMemoryUserDetailsManager() {
}
public InMemoryUserDetailsManager(Collection<UserDetails> users) {
for (UserDetails user : users) {
createUser(user);
}
}
public InMemoryUserDetailsManager(UserDetails... users) {
for (UserDetails user : users) {
createUser(user);
}
}
public InMemoryUserDetailsManager(Properties users) {
Enumeration<?> names = users.propertyNames();
UserAttributeEditor editor = new UserAttributeEditor();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
editor.setAsText(users.getProperty(name));
UserAttribute attr = (UserAttribute) editor.getValue();
Assert.notNull(attr,
() -> "The entry with username '" + name + "' could not be converted to an UserDetails");
createUser(createUserDetails(name, attr));
}
}
private User createUserDetails(String name, UserAttribute attr) {
return new User(name, attr.getPassword(), attr.isEnabled(), true, true, true, attr.getAuthorities());
}
@Override
public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
@Override
public void deleteUser(String username) {
this.users.remove(username.toLowerCase());
}
@Override
public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
@Override
public boolean userExists(String username) {
return this.users.containsKey(username.toLowerCase());
}
@Override
public void changePassword(String oldPassword, String newPassword) {
Authentication currentUser = this.securityContextHolderStrategy.getContext().getAuthentication();
if (currentUser == null) {
// This would indicate bad coding somewhere
throw new AccessDeniedException(
"Can't change password as no Authentication object found in context " + "for current user.");
}
String username = currentUser.getName();
this.logger.debug(LogMessage.format("Changing password for user '%s'", username));
// If an authentication manager has been set, re-authenticate the user with the
// supplied password.
if (this.authenticationManager != null) {
this.logger.debug(LogMessage.format("Reauthenticating user '%s' for password change request.", username));
this.authenticationManager
.authenticate(UsernamePasswordAuthenticationToken.unauthenticated(username, oldPassword));
}
else {
this.logger.debug("No authentication manager set. Password won't be re-checked.");
}
MutableUserDetails user = this.users.get(username);
Assert.state(user != null, "Current user doesn't exist in database.");
user.setPassword(newPassword);
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
mutableUser.setPassword(newPassword);
return mutableUser;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
*
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}
这个类很好理解
在初始化的时候,创建了一个HashMap,所有的用户信息以username作为“主键”都放在了HashMap中。其中
- createUser(UserDetails user)
- updateUser(UserDetails user)
- deleteUser(String username)
- changePassword(String oldPassword, String newPassword)
- userExists(String username)
这些方法来自UserDetailsManager
- loadUserByUsername(String username)
这个方法来自UserDetailsService,而UserDetailsManager继承了UserDetailsService类
配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
废话说的差不多了,开始上配置。在这里,项目启动的时候提供了一个初始用户。也可以直接调用无参构造方法,但是逻辑上不太好弄。然后,Spring Security官方文档中不建议这么创建UserDetails,并且withDefaultPasswordEncoder也是个弃用方法,推荐的创建方法如下
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
上面的密码是password经过Bcrypt加密后的密文
密文可以这么生成
@SpringBootTest
public class ApplicationTest {
@Test
void contextLoads(){
System.out.println(new BCryptPasswordEncoder().encode("password"));
}
}
用的IDEA社区版创建的maven工程,手动引入SpringBoot的依赖,还得手动引入一下test依赖。我嫌从网页下载初始化项目麻烦,结果是遇到了更大的麻烦。还得是因为没钱,用不起付费版,流下了没钱的泪水
后续的用户可以调用createUser等方法来实现创建、删除、修改密码等操作。创建和调用bean的时候最好命名一下