Spring security详细上手教学(二)用户管理
这章节主要学习:
- 如何使用UserDetails接口描述用户
- 在鉴权流中使用UserDetailsService
- 自定义的UserDetailsService实现
- 自定义的UserDetailsManager实现
- 在鉴权中使用JdbcUserDetialsManager
在Spring security中抽象了许多关于用户的接口
- UserDetails,描述了用户
- GrantedAuthority,定义用户可以执行的操作
- UserDetailsManager,扩展了UserDetails,描述了创建用户,修改、删除用户密码等等
在用户管理中,我们使用了UserDetailsService和UserDetailsManager两个接口。
UserDetailsService 只负责将用户信息按照用户名检索出来。这个功能在用户身份认证的时候会被用到。UserDetailsManager继承UserDetailsService ,扩展了增删改用户信息的方法。
1. 实现身份验证
2. 描述用户
2.1 UserDetails接口
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
如果我们不需要设置用户过期等逻辑,那么我们可以直接让isAccountNonExpired返回true即可,同理其他三个接口。
getAuthorities接口返回用户访问资源的权限
isXxxx这四个接口,isXxxNon这种方式好像让人比较困惑,其实这些接口按照身份验证失败返回false,相反返回true这样的逻辑来设定的。
2.2 GrantedAuthority
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
需要实现getAuthority方法来返回权限的名称String。这个接口只有一个方法,所以可以用lambda表达式来实现。
我们还可以使用SimpleGrantedAuthority类来创建。
GrantedAuthority g1 = () -> "READ";
SimpleGrantedAuthority g2 = new SimpleGrantedAuthority("READ");
2.3 UserDetails的最小实现
public class DummyUser implements UserDetails {
private final String username;
private final String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> "read");
}
@Override
public String getPassword() {
return "john";
}
@Override
public String getUsername() {
return "12345";
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2.4 多种用户信息存储方式
实际使用中,我们往往会将用户信息存储在数据库中,或者从外部系统调用用户信息。那么我们就需要将获取到的用户信息和UserDetails结合起来。
如下代码,我们使用jpa的注解将User类的两种职责结合起来
@Entity
public class User implements UserDetails {
@Id
private int id;
private String username;
private String password;
private String authority;
@Override
public String getUsername() {
return this.username;
}
@Override
public String getPassword() {
return this.password;
}
public String getAuthority() {
return this.authority;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> authority);
}
// Omitted code
}
我们还可以利用设计模式中的适配器模式进行解耦,使得我们的类既能适配jps的模型也可以适配UserDetails
public class SecurityUserAdaptor implements UserDetails {
private final User user;
public SecurityUserAdaptor(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(user::getAuthority);
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
3. 如何管理用户
3.1 理解UserDetailsService约定
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
身份验证的事项调用loadUserByUsername获取用户信息。
3.2 实现UserDetailsService
public class InmemoryUserDetailsService implements UserDetailsService {
private final List<UserDetails> users;
public InmemoryUserDetailsService(List<UserDetails> users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.stream()
.filter(user -> user.getUsername().equals(username))
.findFirst()
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
这里自定义了一个用户list将用户信息保存在内存中。
loadUserByUsername方法从List中根据用户名筛选出来用户信息
3.3 UserDetailsManager扩展UserDetailsService接口
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
3.4 JdbcUserDetailsManager
需要先引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
JdbcUserDetailsManager类管理存储在SQL数据库中的用户信息,通过JDBC链接数据库。
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsManager(DataSource dataSource) {
String usersByUsernameQuery = "select username, password, enabled [CA] from users where username = ?";
String authsByUserQuery = "select username, authority [CA] from spring.authorities where username = ?";
var userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
3.5 LdapUserDetailsManager
LDAP没接触过,暂时省略