在 Spring Security 默认配置中,Spring Security 会为我们提供一个默认用户 “user”,以及在项目启动时会为这个用户生成一串 UUID 字符串密码。但是在实际的项目中,都是在我们的数据库中获取用户信息来进行登陆的。所以在 Spring Security 配置数据库数据源是非常重要的配置。

那接下来就看一看在 Spring Security 中如何连接数据库,使用数据库数据登陆吧!

本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples

本系列的其他文章,推荐按顺序阅读:

  1. Spring Security 使用 JSON 格式登陆

你将会学到什么

  1. Spring Security 默认的用户配置,默认用户从哪里来?到哪里去?
  2. Spring Security 是如何获取用户信息进行身份验证的?
  3. Spring Security 中用户信息的表示方式是什么?
  4. 如何自定义用户信息数据源,接入数据库数据?
  5. Spring Security 的密码加密是什么?

1. Spring Security 的默认用户配置

1.1 创建 Spring Security 项目

首先需要创建一个 Spring Security 的项目。你可以使用 Spring Initializr 进行创建,也可以使用 Maven 进行创建。因为以后可能还会继续写关于 Spring Security 相关的示例,所以本文配套的源码是使用 Maven 创建的一个多模块项目,以后的示例都会放到这个项目中。

spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
数据库相关依赖
<!-- 本文使用 MyBatis-Plus 连接数据库 -->
<!-- 使用什么的方法都可以,只要从数据库查询到数据就行 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

编写一个用于测试的接口:

Application.java
@SpringBootApplication
@MapperScan("com.lxiaocode.security.mapper")
@RestController
@RequestMapping("/")
public class DatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(DatabaseApplication.class);
}

@GetMapping("")
public String index(){
return "index.html";
}
}

1.2 Spring Security 默认用户配置

在我的上一篇文章对 Spring Security 默认配置的介绍中,有一部分是关于 Spring Security 默认用户配置的:

1.2 Spring Security 默认配置

  • 省略 …
  • 使用用户名和随机生成的密码创建一个 UserDetailsService bean,并记录到控制台。

1.2.1 默认配置实现的功能

  • 省略 …
  • 让用户使用 user 用户名和密码通过基于表单的身份验证(再前面的示例中,密码为 8e557245-73e2-4286-969a-ff57fe326336)。

但是当时只是一笔带过,接下来就开始进一步的研究 Spring Security 是如何为我们提供这个默认用户的。

1.2.1 基于内存的用户信息管理

UserDetailsServiceAutoConfiguration:

因为在默认 Spring Security 配置用户并没连接数据库,所以 Spring Security 默认配置就采取了将用户信息保存在内存中的策略。Spring Security 有关默认用户的配置就在 UserDetailsServiceAutoConfiguration 中:

UserDetailsServiceAutoConfiguration.java
public class UserDetailsServiceAutoConfiguration {

private static final String NOOP_PASSWORD_PREFIX = "{noop}";

private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

// 这个就是 Spring Security 提供的默认的 InMemoryUserDetailsManager
// 里面保存了默认用户的信息,你也可以自己往里面添加用户信息
@Bean
@ConditionalOnMissingBean(
type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
// 从传入的参数 properties 获取默认用户,并加入到 InMemoryUserDetailsManager
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}

// 很明显这个方法就是获取默认用户密码,并将它打印到控制台中的方法
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
// 在这里会从传入的参数 user 中获取密码
String password = user.getPassword();
// 判断用户是否开启了生成密码,如果开启了就将生成的密码打印到控制台
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
return NOOP_PASSWORD_PREFIX + password;
}
}

根据上面源码的解析,我知道默认用户是保存在默认提供的 InMemoryUserDetailsManager 实例中,并且会使用 getOrDeducePassword() 方法获取密码以及打印生成的密码。

SecurityProperties:

在创建 InMemoryUserDetailsManager bean 时有一个很关键的参数 SecurityProperties ,默认用户的信息都是从这里面来的:

SecurityProperties.java
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
// 省略其他方法和字段...

// 这是一个内部类,Spring Security 的默认用户信息就是从这里来的
public static class User {

// 默认用户的用户名
private String name = "user";

// 默认用户的密码,可以看出这是一个 UUID 字符串
private String password = UUID.randomUUID().toString();

// 角色列表
private List<String> roles = new ArrayList<>();

// 密码生成标记
private boolean passwordGenerated = true;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public String getPassword() {
return this.password;
}

// 用户密码的 setter 方法
// 假如你在 InMemoryUserDetailsManager 添加了自定义用户,就会使用这个方法设置用户的密码
public void setPassword(String password) {
if (!StringUtils.hasLength(password)) {
return;
}
// 将默认密码生成标记设置为 false
// 也就是说,如果你添加了自定义用户,生成的 UUID 密码就会被覆盖
// 在控制台就不会再打印生成的 UUID 密码了
this.passwordGenerated = false;
this.password = password;
}

public List<String> getRoles() {
return this.roles;
}

public void setRoles(List<String> roles) {
this.roles = new ArrayList<>(roles);
}

public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
}

从上两个类,我们知道默认用户的信息是从 SecurityProperties.User 中来的,并且会在 UserDetailsServiceAutoConfiguration 中添加到 InMemoryUserDetailsManager,并将生成的密码打印到控制台(passwordGenerated = true)。

2. 获取用户信息进行身份验证

我们解决了 Spring Security 的默认用户从哪来,到哪去的问题,但是还有一个重要的问题没有解决。那就是Spring Security 在身份验证的时候如何获取用户的信息的?

2.1 从内存中获取用户信息

因为 Spring Security 默认是使用内存保存用户信息的,所以在身份验证是肯定会从内存中获取用户信息。

在进行身份验证时,Spring Security 会调用多个身份验证支持(策略),DaoAuthenticationProvider 就是其中一个。通常都会调用 DaoAuthenticationProvider 来进行身份验证,其中有一个重要的方法 retrieveUser() 。从名字来看这很有可能是用来查询用户的方法:

DaoAuthenticationProvider.java
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

// 省略其他方法和字段...

// 密码加密器
private PasswordEncoder passwordEncoder;

// 默认配置下,这个字段保存的就是上面提到的 InMemoryUserDetailsManager
// InMemoryUserDetailsManager 保存着内存中的用户
private UserDetailsService userDetailsService;

// 检索用户
// 这个方法会被传入两个参数,一个是用户登陆是输入的用户名
// 另一个是使用用户登陆输入进行封装的 Authentication 实例
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {

// 这个 getUserDetailsService() 获取的就是 Spring Security 提供的 InMemoryUserDetailsManager
// 使用用户名从 loadUserByUsername() 方法获取 UserDetails
// UserDetails 保存的就是用户的信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
}

从上面源码中可以看到,DaoAuthenticationProvider 保存着一个 UserDetailsService 接口的字段,这个字段保存的就是 Spring Security 默认提供的 InMemoryUserDetailsManager。然后从中获取内存中的用户信息进行返回,接着进行身份验证。

所以,如果我们提供自定义的 UserDetailsService 接口,然后实现其中的 loadUserByUsername() 方法,在这个方法中通过查询数据库返回指定用户,就可以实现使用数据库的用户数据进行登陆了。

2.2 从数据库中获取用户信息

2.2.1 Spring Security 用户数据表示方式

UserDetails:

在从数据库中获取用户信息前,我们先要介绍 UserDetails。因为 Spring Security 是不认识程序员自己定义的用户类的,所以 Spring Security 提供了一个 UserDetails 接口用来表示用户类。我们需要实现这个接口,并将用户的数据保存进这个接口的实现类中,Spring Security 就可以利用 UserDeatils 来获取用户信息了。

UserDetails.java
public interface UserDetails extends Serializable {

// 权限列表
Collection<? extends GrantedAuthority> getAuthorities();
// 密码
String getPassword();
// 用户名
String getUsername();
// 账号是否过期
boolean isAccountNonExpired();
// 账号是否锁定
boolean isAccountNonLocked();
// 账号凭证是否未过期
boolean isCredentialsNonExpired();
// 账号是否可用
boolean isEnabled();
}

实现 UserDetails:

你可以单独实现 UserDetails 接口与用户类分开,也可以直接在用户类中实现该接口。在这个示例中我直接在用户类中实现这个接口了:

User.java
@TableName("user")
public class User implements UserDetails {

private int id;
private String username;
private String password;

/**
* 表示该属性不为数据库表字段,但又是必须使用的。
*/
@TableField(exist = false)
private Set<? extends GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public String getPassword() {
return this.password;
}

@Override
public String getUsername() {
return this.username;
}

public void setAuthorities(Set<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}

//账号是否过期
@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.2.2 自定义 UserDetailsService

UserDetailsService:

有了 UserDetails 对用户数据的封装,接下来就可以实现 UserDetailsService 接口了:

UserDetailsService.java
public interface UserDetailsService {

// 在这个接口中只有一个方法需要实现
// 就是这个根据用户名加载用户
// 其实不一定是用户名,可以是手机号、邮箱等可以用来确定唯一用户的信息都可以
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

实现 UserDetailsService:

UserService.java
@Service
public class UserService implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里是使用 MyBatis-Plus 进行数据库查询
// 你可以选择自己喜欢的方式进行数据库查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}
// 因为在这个示例中 User 本身就是 UserDetails,所以可以直接返回
// 如果不是,则需要将用户类的信息拷贝到 UserDeatils 中,然后将 UserDeatils 返回
return user;
}
}

2.3 配置 UserDetailsService

接下来就是要将 UserDetailsService 配置到 Spring Security 中去:

SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// 注入 UserService
@Autowired
private UserService userService;

// 用于密码加解密,如果你需要自定义用户则需要提供一个 PasswordEncoder
// 在身份校验时 Spring Security 会使用它来对密码进行解密
// 在获取用户密码时,密码必须是加密状态的,所以在用户注册时也需要使用 PasswordEncoder 进行加密
// 否则 PasswordEncoder 向一个未加密的密码进行解密,会导致密码错误
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 将 userService 设置到 AuthenticationManagerBuilder 即可
auth.userDetailsService(userService);
}
}

接下来就可以使用数据库中保存的用户信息进行登陆了。。

3. 总结

以上就是在 Spring Security 连接数据库的所有内容。

  • Spring Security 的默认用户在 SecurityProperties.User 中生成并会在 UserDetailsServiceAutoConfiguration 中在用户信息保存到 InMemoryUserDetailsManager 中。
  • 身份验证时会使用 UserDetailsService.loadUserByUsername() 加载用户数据。
  • 在 Spring Security 中用户数据都是以 UserDetails 表示的。

本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples

评论