# 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（轻量级目录访问协议）接入的用户存储。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v5/di-4-zhang-spring-an-quan/4.2-pei-zhi-spring-security/4.2.2-ji-yu-jdbc-de-yong-hu-cun-chu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
