# 4.2.2 基于 JDBC 的用户存储

用户信息通常在关系数据库中维护，基于 JDBC 的用户存储似乎比较合适。下面的程序清单显示了如何配置 Spring Security，并将用户信息通过 JDBC 保存在关系型数据库中，来进行身份认证。

```java
@Autowired
DataSource dataSource;
​
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .jdbcAuthentication()
        .dataSource(dataSource);
}
```

configure() 的这个实现在给定的 AuthenticationManagerBuilder 上调用 jdbcAuthentication()。然后，必须设置 DataSource，以便它知道如何访问数据库。这里使用的数据源是由自动装配提供的。

**重写默认用户查询**

虽然这个最小配置可以工作，但它对数据库模式做了一些假设。它期望已经存在某些表，用户数据将保存在这些表中。更具体地说，以下来自 Spring Security 内部的代码片段显示了在查找用户详细信息时将执行的 SQL 查询：

```java
public static final String DEF_USERS_BY_USERNAME_QUERY = 
    "select username,password,enabled " +
    "from users " +
    "where username = ?";
​
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
    "select username,authority " +
    "from authorities " +
    "where username = ?";
​
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
    "select g.id, g.group_name, ga.authority " +
    "from groups g, group_members gm, group_authorities ga " +
    "where gm.username = ? " +
    "and g.id = ga.group_id " +
    "and g.id = gm.group_id";
```

第一个查询检索用户的用户名、密码以及是否启用它们，此信息用于对用户进行身份验证；下一个查询查询用户授予的权限，以进行授权；最后一个查询查询作为组的成员授予用户的权限。

如果可以在数据库中定义和填充满足这些查询的表，那么就没有什么其他要做的了。但是，数据库很可能不是这样的，需要对查询进行更多的控制。在这种情况下，可以配置自己的查询。程序清单 4.4 自定义用户详情查询

```java
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(
                "select username, password, enabled from Users " +
                "where username=?")
            .authoritiesByUsernameQuery(
                "select username, authority from UserAuthorities " +
                "where username=?");
}
```

在本例中，仅重写了身份验证和基本授权查询，也可以通过使用自定义查询调用 groupAuthoritiesByUsername() 来重写组权限查询。

在将默认 SQL 查询替换为自己设计的查询时，一定要遵守查询的基本约定。它们都以用户名作为唯一参数。身份验证查询选择用户名、密码和启用状态；授权查询选择包含用户名和授予的权限的零个或多个行的数据；组权限查询选择零个或多个行数据，每个行有一个 group id、一个组名和一个权限。

**使用编码密码**

以身份验证查询为重点，可以看到用户密码应该存储在数据库中。唯一的问题是，如果密码以纯文本形式存储，就会受到黑客的窥探。但是如果在数据库中对密码进行编码，身份验证将失败，因为它与用户提交的明文密码不匹配。

为了解决这个问题，你需要通过调用 passwordEncoder() 方法指定一个密码编码器：

```java
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(
                "select username, password, enabled from Users " +
                "where username=?")
            .authoritiesByUsernameQuery(
                "select username, authority from UserAuthorities " +
                "where username=?")
            .passwordEncoder(new StandardPasswordEncoder("53cr3t");
}
```

passwordEncoder() 方法接受 Spring Security 的 passwordEncoder 接口的任何实现。Spring Security 的加密模块包括几个这样的实现：

* BCryptPasswordEncoder —— 采用 bcrypt 强哈希加密
* NoOpPasswordEncoder —— 不应用任何编码
* Pbkdf2PasswordEncoder —— 应用 PBKDF2 加密
* SCryptPasswordEncoder —— 应用了 scrypt 散列加密
* StandardPasswordEncoder —— 应用 SHA-256 散列加密

上述代码使用了 StandardPasswordEncoder。但是，如果没有现成的实现满足你的需求，你可以选择任何其他实现，甚至可以提供你自己的自定义实现。PasswordEncoder 接口相当简单：

```java
public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
}
```

无论使用哪种密码编码器，重要的是要理解数据库中的密码永远不会被解码。相反，用户在登录时输入的密码使用相同的算法进行编码，然后将其与数据库中编码的密码进行比较。比较是在 PasswordEncoder 的 matches() 方法中执行的。

最后，将在数据库中维护 Taco Cloud 用户数据。但是，我没有使用 jdbcAuthentication()，而是想到了另一个身份验证选项。但在此之前，让我们先看看如何配置 Spring Security 以依赖于另一个常见的用户数据源：使用 LDAP（轻量级目录访问协议）接入的用户存储。
