vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》基于 JDBC 的身份验证

基于 JDBC 的身份验证

在上一章中,我们看到了如何扩展 Spring Security 以利用我们的 CalendarDao 接口和我们现有的域模型来验证用户。在本章中,我们将看到如何使用 Spring Security 内置的 JDBC 支持。为简单起见,本章的示例代码基于 第 2 章中的 Spring Security 设置Spring Security 入门。在本章中,我们将介绍以下主题:

  • Using Spring Security's built-in JDBC-based authentication support
  • Utilizing Spring Security's group-based authorization to make administering users easier
  • Learning how to use Spring Security's UserDetailsManager interface
  • Configuring Spring Security to utilize the existing CalendarUser schema to authenticate users
  • Learning how we can secure passwords using Spring Security's new cryptography module
  • Using Spring Security's default JDBC authentication

如果您的应用程序尚未实现安全性,或者您的安全性基础设施正在使用数据库,那么 Spring Security 提供了开箱即用的支持,可以简化您的安全需求的解决。 Spring Security 为用户、权限和组提供了默认模式。如果这不能满足您的需求,它允许自定义用户的查询和管理。在下一节中,我们将介绍使用 Spring Security 设置 JDBC 身份验证的基本步骤。

必需的依赖项

我们的应用程序已经定义了本章所需的所有必要依赖项。但是,如果您使用 Spring Security 的 JDBC 支持,您可能希望在 build.gradle 文件中列出以下依赖项。需要强调的是,您将使用的 JDBC 驱动程序将取决于您使用的数据库。有关数据库需要哪个驱动程序的详细信息,请参阅数据库供应商的文档。

记住,所有的 Spring 版本都需要匹配,所有的 Spring Security 版本都需要匹配(这包括传递依赖版本)。如果您无法在自己的应用程序中使用它,您可能需要在 build.gradle 中定义依赖管理部分来强制执行此操作,如 第 2 章Spring Security 入门。如前所述,使用示例代码时您无需担心这一点,因为我们已经为您设置了必要的依赖项。

以下代码段定义了本章所需的依赖项,包括 Spring Security 和 JDBC 依赖项:

    //build.gradle

    dependencies {
    ...
    // Database:
     compile('org.springframework.boot:spring-boot-starter-jdbc')
     compile('com.h2database:h2')
    // Security:
     compile('org.springframework.boot:spring-boot-starter-security')
     testCompile('org.springframework.security:spring-security-test')
       ....
    }

使用 H2 数据库

本练习的第一部分涉及设置基于 Java 的 H2 关系数据库的实例,填充 Spring Security 默认模式。我们将使用 Spring 的 EmbeddedDatabase 配置功能将 H2 配置为在内存中运行——这是一种比手动设置数据库简单得多的配置方法。
您可以在 H2 网站 http://www.h2database.com/ 上找到更多信息。

请记住,在我们的示例应用程序中,我们将主要使用 H2,因为它易于设置。 Spring Security 可以与任何支持 ANSI SQL 的数据库一起使用。如果您按照示例进行操作,我们鼓励您调整配置并使用您喜欢的数据库。由于我们不希望本书的这一部分专注于数据库设置的复杂性,因此我们选择了方便而不是现实来进行练习。

提供的 JDBC 脚本

我们在 src/main/resources/database/h2/ 文件夹中提供了所有用于在 H2 数据库中创建模式和数据的 SQL 文件。任何以 security 为前缀的文件都是为了支持 Spring Security 的默认 JDBC 实现。任何以 calendar 为前缀的 SQL 文件都是 JBCP 日历应用程序的自定义 SQL 文件。希望这将使运行示例更容易一些。如果您使用自己的数据库实例,则可能必须调整架构定义语法以适应您的特定数据库。其他数据库模式可以在 Spring Security 参考中找到。您可以在本书的附录其他参考资料中找到指向 Spring Security Reference 的链接。

配置 H2 嵌入式数据库

要配置 H2 嵌入式数据库,我们需要创建一个 DataSource 并运行 SQL 来创建 Spring Security 表结构。我们将需要更新在启动时加载的 SQL,以包括 Spring Security 的基本模式定义、Spring Security 用户定义和用户的权限映射。您可以在以下代码片段中找到 DataSource 定义和相关更新:

    //src/main/java/com/packtpub/springsecurity/configuration/DataSourceConfig.java

    @Bean
    public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
       .setName("dataSource")
       .setType(EmbeddedDatabaseType.H2)
       .addScript("/database/h2/calendar-schema.sql")
       .addScript("/database/h2/calendar-data.sql")
       .addScript("/database/h2/security-schema.sql")
       .addScript("/database/h2/security-users.sql")
       .addScript("/database/h2/security-user-authorities.sql")
       .build();
    }

请记住,EmbeddedDatabaseBuilder() 方法仅在内存中创建此数据库,因此您不会在磁盘上看到任何内容,也无法使用标准工具对其进行查询。但是,您可以使用嵌入在应用程序中的 H2 控制台与数据库进行交互。请参阅我们应用程序的 Welcome 页面上的说明以了解如何使用它。

配置 JDBC UserDetailsManager 实现

我们将修改 SecurityConfig.java 文件以声明我们正在使用 JDBC UserDetailsManager 实现,而不是 Spring Security in-memory UserDetailsS​​ervice 我们在第2章中配置的实现,入门弹簧安全。这是通过对 UserDetailsManager 声明的简单更改来完成的,如下所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    …
    @Bean
    @Override
    public UserDetailsManager userDetailsService() {
       JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
       manager.setDataSource(dataSource);
       return manager;
    }
    …

我们将之前的 configure(AuthenticationManagerBuilder) 方法以及所有子元素替换为 userDetailsS​​ervice() 方法,如前面的代码片段所示。

Spring Security 的默认用户模式

让我们看一下用于初始化数据库的每个 SQL 文件。我们添加的第一个脚本包含用户及其权限的默认 Spring Security 模式定义。以下脚本改编自 Spring Security 的参考,该参考在 附录附加参考资料中列出,具有明确命名的约束,以使故障排除更容易:

    //src/main/resources/database/h2/security-schema.sql

    create table users(
       username varchar(256) not null primary key,
       password varchar(256) not null,
       enabled boolean not null
    );
    create table authorities (
       username varchar(256) not null,
       authority varchar(256) not null,
       constraint fk_authorities_users
           foreign key(username) references users(username)
    );
    create unique index ix_auth_username on authorities (username,authority);

定义用户

下一个脚本负责在我们的应用程序中定义用户。包含的 SQL 语句创建了我们迄今为止在整本书中使用的相同用户。该文件还添加了一个额外的用户 [email protected],由于我们将该用户指示为已禁用,因此该用户将无法登录:

    //src/main/resources/database/h2/security-users.sql

    insert into users (username,password,enabled)
       values ('[email protected]','user1',1);
    insert into users (username,password,enabled)
       values ('[email protected]','admin1',1);
    insert into users (username,password,enabled)
       values ('[email protected]','admin1',1);
    insert into users (username,password,enabled)
       values ('[email protected]','disabled1',0);

定义用户权限

您可能已经注意到,没有指示用户是管理员还是普通用户。下一个文件指定用户到相应权限的直接映射。如果用户没有映射到它的权限,Spring Security 将不允许该用户登录:

    //src/main/resources/database/h2/security-user-authorities.sql

    insert into authorities(username,authority)
       values ('[email protected]','ROLE_USER');
    insert into authorities(username,authority)
      values ('[email protected]','ROLE_ADMIN');
    insert into authorities(username,authority)
       values ('[email protected]','ROLE_USER');
    insert into authorities(username,authority)
       values ('[email protected]','ROLE_USER');
    insert into authorities(username,authority)
       values ('[email protected]','ROLE_USER');

将 SQL 添加到嵌入式数据库配置后,我们应该能够启动应用程序并登录。尝试使用 [email protected] 作为 username< 的新用户登录/kbd> 和 disabled1 作为 密码。请注意,Spring Security 不允许用户登录并提供错误消息 Reason: User is disabled

您的代码现在应该如下所示:calendar04.01-calendar

UserDetailsManager 接口

我们已经在 章节中利用了 Spring Security 中的 InMemoryUserDetailsManager 类3自定义认证,在我们UserContextSpringSecurityUserContext实现中查找当前的CalendarUser应用.这使我们能够确定在查找 My Events 页面的事件时应该使用哪个 CalendarUser第 3 章自定义身份验证,也演示了如何更新 DefaultCalendarService.java 文件以利用 InMemoryUserDetailsManager,以确保我们在创建 CalendarUser 时创建了新的 Spring Security 用户。本章重用了完全相同的代码。唯一的区别是 UserDetailsManager 实现由 Spring Security 的 JdbcUserDetailsManager 类支持,该类使用数据库而不是内存数据存储。

UserDetailsManager 还提供哪些开箱即用的其他功能?

尽管这些类型的函数使用额外的 JDBC 语句相对容易编写,但 Spring Security 实际上提供了开箱即用的功能来支持许多常见的创建、读取、更新和删除( CRUD) 对 JDBC 数据库中用户的操作。这对于简单的系统来说很方便,并且为用户可能拥有的任何自定义需求奠定了良好的基础:

方法

说明

void createUser(UserDetails 用户)

它使用给定的 UserDetails 信息创建一个新用户,包括任何声明的 GrantedAuthority 权限。

void updateUser(最终 UserDetails 用户)

它使用给定的 UserDetails 信息更新用户。它更新 GrantedAuthority 并从用户缓存中删除用户。

void deleteUser(字符串用户名)

它删除具有给定用户名的用户,并从用户缓存中删除该用户。

boolean userExists(String username)

指示用户(活动或非活动)是否存在具有给定用户名的用户。

void changePassword(String oldPassword, String newPassword)

它更改当前登录用户的密码。然后,用户必须提供正确的密码才能使操作成功。

如果 UserDetailsManager 没有提供应用程序所需的所有方法,您可以扩展接口以提供这些自定义要求。例如,如果您需要能够在管理视图中列出所有可能的用户,您可以使用此方法编写自己的接口并提供一个指向与 UserDetailsManager 实现相同的数据存储的实现您当前正在使用。

基于组的访问控制

JdbcUserDetailsManager 类支持通过将 GrantedAuthority 分组到称为组的逻辑集来添加用户和 GrantedAuthority 声明之间的间接级别的能力。

然后为用户分配一个或多个组,并且他们的成员资格授予一组 GrantedAuthority 声明:

读书笔记《spring-security-third-edition》基于 JDBC 的身份验证

正如您在上图中所见,这种间接方式允许将同一组角色分配给多个用户,只需将任何新用户分配给现有组即可。这是迄今为止我们看到的不同行为,之前我们将 GrantedAuthority 直接分配给单个用户。

这种通用权限集的捆绑在以下情况下可能会有所帮助:

  • You need to segregate users into communities, with some overlapping roles between groups.
  • You want to globally change the authorization for a class of user. For example, if you have a supplier group, you might want to enable or disable their access to particular portions of the application.
  • You have a large number of users, and you don't need user-level authority configuration.

除非您的应用程序的用户群非常小,否则您很有可能会使用基于组的访问控制。虽然基于组的访问控制比其他策略稍微复杂一些,但管理用户访问的灵活性和简单性使得这种复杂性是值得的。这种按组聚合用户权限的间接技术通常称为基于组的访问控制 (GBAC)。

GBAC 是市场上几乎所有安全操作系统或软件包通用的方法。 Microsoft Active Directory (AD) 是大规模 GBAC 最明显的实现之一,因为它的设计是将 AD 用户划分为组并为这些组分配权限。通过使用 GBAC,大型基于 AD 的组织的权限管理变得更加简单。

试着想想你使用的软件的安全模型——用户、组和权限是如何管理的?编写安全模型的方式有哪些优缺点?

让我们为 JBCP 日历应用程序添加一个抽象级别,并将基于组的授权概念应用于站点。

配置基于组的访问控制

我们将向应用程序添加两个组:普通用户,我们称之为Users,和管理用户,我们称之为Administrators。我们现有的帐户将通过附加的 SQL 脚本与适当的组相关联。

配置 JdbcUserDetailsManager 以使用组

默认情况下,Spring Security 不使用 GBAC。因此,我们必须指示 Spring Security 启用组的使用。修改 SecurityConfig.java 文件以使用 GROUP_AUTHORITIES_BY_USERNAME_QUERY,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    private static String 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";
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth
       .jdbcAuthentication()
       .dataSource(dataSource)
       .groupAuthoritiesByUsername(
         GROUP_AUTHORITIES_BY_USERNAME_QUERY
     );
    }

使用 GBAC JDBC 脚本

接下来,我们需要更新启动时正在加载的脚本。我们需要删除 security-user-authorities.sql 映射,以便我们的用户不再通过直接映射获得他们的权限。然后我们需要添加两个额外的 SQL 脚本。更新 DataSource bean 配置以加载 GBAC 所需的 SQL,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/DataSourceConfig.java

    @Bean
    public DataSource dataSource() {
       return new EmbeddedDatabaseBuilder()
         .setName("dataSource")
         .setType(EmbeddedDatabaseType.H2)
         .addScript("/database/h2/calendar-schema.sql")
         .addScript("/database/h2/calendar-data.sql")
         .addScript("/database/h2/security-schema.sql")
         .addScript("/database/h2/security-users.sql")
         .addScript("/database/h2/security-groups-schema.sql")
         .addScript("/database/h2/security-groups-mappings.sql")
         .build();
    }

基于组的模式

这可能很明显,但我们添加的第一个 SQL 文件包含对模式的更新以支持基于组的授权。您可以在以下代码段中找到该文件的内容:

    //src/main/resources/database/h2/security-groups-schema.sql

    create table groups (
    id bigint generated by default as identity(start with 0) primary key,
    group_name varchar(256) not null
    );
    create table group_authorities (
      group_id bigint not null,
      authority varchar(256) not null,
      constraint fk_group_authorities_group
      foreign key(group_id) references groups(id)
    );
    create table group_members (
      id bigint generated by default as identity(start with 0) primary key,
      username varchar(256) not null,
      group_id bigint not null,\
      constraint fk_group_members_group
      foreign key(group_id) references groups(id)\
    );

组权限映射

现在我们需要将现有用户映射到组,并将组映射到权限。这是在 security-groups-mappings.sql 文件中完成的。基于组的映射可能很方便,因为由于各种原因,组织通常已经拥有一个逻辑用户组。通过利用现有的用户分组,我们可以大大简化我们的配置。这就是间接层如何帮助我们的方式。我们在以下组映射中包含了组定义、组到权限映射和一些用户:

    //src/main/resources/database/h2/security-groups-mappings.sql

    -- Create the Groups

    insert into groups(group_name) values ('Users');
    insert into groups(group_name) values ('Administrators');

    -- Map the Groups to Roles

    insert into group_authorities(group_id, authority)
    select id,'ROLE_USER' from groups where group_name='Users';
    insert into group_authorities(group_id, authority)
    select id,'ROLE_USER' from groups where
    group_name='Administrators';
    insert into group_authorities(group_id, authority)
    select id,'ROLE_ADMIN' from groups where
    group_name='Administrators';

    -- Map the users to Groups

    insert into group_members(group_id, username)
    select id,'[email protected]' from groups where
    group_name='Users';
    insert into group_members(group_id, username)
    select id,'[email protected]' from groups where
    group_name='Administrators';
    ...

继续并启动应用程序,它会像以前一样运行;但是,用户和角色之间的附加抽象层简化了对大量用户的管理。

您的代码现在应该类似于 calendar04.02-calendar

支持自定义架构

Spring Security 的新用户通常通过将 JDBC 用户、组或角色映射适应现有模式来开始他们的体验。即使遗留数据库不符合预期的 Spring Security 模式,我们仍然可以配置 JdbcDaoImpl 以映射到它。

我们现在将更新 Spring Security 的 JDBC 支持以使用我们现有的 CalendarUser 数据库以及新的 calendar_authorities 表。

我们可以轻松更改 JdbcUserDetailsManager 的配置以利用此模式并覆盖 Spring Security 的预期表定义和列,我们将其用于 JBCP 日历应用程序。

确定正确的 JDBC SQL 查询

JdbcUserDetailsManager 类具有三个 SQL 查询,它们具有明确定义的参数和一组返回的列。我们必须根据预期的功能确定我们将分配给每个查询的 SQL。 JdbcUserDetailsManager 使用的每个 SQL 查询都将登录时显示的用户名作为其唯一的参数:

命名空间查询属性名称

说明

预期的 SQL 列

用户按用户名查询

返回与用户名匹配的一个或多个用户;仅使用第一个用户。

用户名 (字符串)

密码 (字符串)

启用布尔值

authorities-by-username-query

返回直接提供给用户的一个或多个授予权限。通常在禁用 GBAC 时使用。

用户名 (字符串)

GrantedAuthority (字符串)

group-authorities-by-username-query

返回通过组成员资格提供给用户的授予权限和组详细信息。启用 GBAC 时使用。

组主键(任意)

组名(任意)

GrantedAuthority(字符串)

请注意,在某些情况下,默认 JdbcUserDetailsManager 实现不使用返回列,但无论如何都必须返回它们。

更新加载的 SQL 脚本

我们需要使用我们的自定义模式来初始化 DataSource,而不是使用 Spring Security 的默认模式。更新 DataSourceConfig.java 文件,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/DataSourceConfig.java

    @Bean
    public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
       .setName("dataSource")
     .setType(EmbeddedDatabaseType.H2)
       .addScript("/database/h2/calendar-schema.sql")
       .addScript("/database/h2/calendar-data.sql")
      .addScript("/database/h2/calendar-authorities.sql")
       .build();
    }

请注意,我们已经删除了所有以 security 开头的脚本,并将它们替换为 calendar-authorities.sql

CalendarUser 权限 SQL

您可以在以下代码片段中查看 CalendarUser 权限映射:

    //src/main/resources/database/h2/calendar-authorities.sql

    create table calendar_user_authorities (
       id bigint identity,
       calendar_user bigint not null,
       authority varchar(256) not null,
    );
    -- [email protected]
    insert into calendar_user_authorities(calendar_user, authority)
       select id,'ROLE_USER' from calendar_users where
       email='[email protected]';
    -- [email protected]
    insert into calendar_user_authorities(calendar_user, authority)
       select id,'ROLE_ADMIN' from calendar_users where     
       email='[email protected]';
    insert into calendar_user_authorities(calendar_user, authority)
       select id,'ROLE_USER' from calendar_users where
       email='[email protected]';
    -- [email protected]
    insert into calendar_user_authorities(calendar_user, authority)
       select id,'ROLE_USER' from calendar_users where
     email='[email protected]';
请注意,我们使用 id 作为外键,这比使用用户名作为外键(如 Spring Security 所做的那样)要好。通过使用 id 作为外键,我们可以让用户轻松更改他们的用户名。

插入自定义权限

当我们添加一个新的 CalendarUser 类时,我们需要更新 DefaultCalendarService 以使用我们的自定义模式为用户插入权限。这是因为虽然我们重用了用户定义的模式,但我们没有在现有应用程序中定义自定义权限。更新DefaultCalendarService,如下:

    //src/main/java/com/packtpub/springsecurity/service/DefaultCalendarService.java

    import org.springframework.jdbc.core.JdbcOperations;
    ...
    public class DefaultCalendarService implements CalendarService {
       ...
       private final JdbcOperations jdbcOperations;
       @Autowired
          public DefaultCalendarService(EventDao eventDao, 
          CalendarUserDao userDao, JdbcOperations jdbcOperations) {
           ...
           this.jdbcOperations = jdbcOperations;
       }
       ...
       public int createUser(CalendarUser user) {
           int userId = userDao.createUser(user);
           jdbcOperations.update(
             "insert into calendar_user_authorities(calendar_user,authority) 
             values(?,?)", userId, "ROLE_USER");
           return userId;
       }
    }
您可能已经注意到用于插入我们的用户的 JdbcOperations 接口。这是 Spring 提供的一个方便的模板,可帮助管理样板代码,例如连接和事务处理。有关详细信息,请参阅本书的附录附加参考资料以查找 Spring 参考。

配置 JdbcUserDetailsManager 以使用自定义 SQL 查询

为了对我们的非标准模式使用自定义 SQL 查询,我们将简单地更新我们的 userDetailsS​​ervice() 方法以包含新查询。这与我们启用对 GBAC 支持的方式非常相似,只是我们将使用修改后的 SQL,而不是使用默认 SQL。请注意,我们删除了旧的 setGroupAuthoritiesByUsernameQuery() 方法调用,因为我们不会在此示例中使用它,以保持简单:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    private static String CUSTOM_USERS_BY_USERNAME_QUERY = ""+
                               "select email, password, true " +
                              "from calendar_users where email = ?";
    private static String CUSTOM_AUTHORITIES_BY_USERNAME_QUERY = ""+
               "select cua.id, cua.authority " +
               "from calendar_users cu, calendar_user_authorities "+
               "cua where cu.email = ? "+
               "and cu.id = cua.calendar_user";
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
       .jdbcAuthentication()
       .dataSource(dataSource)
       .usersByUsernameQuery(USERS_BY_USERNAME_QUERY)
       .authoritiesByUsernameQuery(
         AUTHORITIES_BY_USERNAME_QUERY
       );
    }

这是使用 Spring Security 从现有的非默认模式中读取设置所需的唯一配置!启动应用程序并确保一切正常。

您的代码现在应该如下所示: calendar04.03-日历

请记住,使用现有模式通常需要扩展 JdbcUserDetailsManager 以支持更改密码、重命名用户帐户和其他用户管理功能。

如果您使用 JdbcUserDetailsManager 来执行用户管理任务,那么该类使用了 20 多个 SQL 查询,可以通过配置访问这些查询。但是,通过命名空间配置只能使用所涵盖的三个。请参阅 Javadoc 或源代码以查看 JdbcUserDetailsManager 使用的查询的默认值。

配置安全密码

您可能还记得 第 1 章中的安全审计,解剖不安全的应用程序,以明文形式存储的密码的安全性是审核员的首要任务。事实上,在任何安全系统中,密码安全性都是经过身份验证的主体的信任和权威性的一个关键方面。完全安全系统的设计者必须确保密码的存储方式使恶意用户难以破解密码。

以下一般规则应适用于存储在数据库中的密码:

  • Passwords must not be stored in cleartext (plaintext)
  • Passwords supplied by the user must be compared to the recorded passwords in the database
  • A user's password should not be supplied to the user upon demand (even if the user forgets it)

对于大多数应用程序而言,最适合这些要求的是单向编码,称为密码的散列。使用加密哈希提供了安全性和唯一性等属性,这些属性对于正确验证用户非常重要,另外还有一个好处是,一旦对它进行哈希处理,就无法从存储的值中提取密码。

在大多数安全应用程序设计中,既不需要也不需要根据请求检索用户的实际密码,因为在没有适当的附加凭据的情况下向他们提供用户密码可能会带来重大的安全风险。相反,大多数应用程序为用户提供了重置密码的能力,方法是提供额外的凭据(例如他们的社会安全号码、出生日期、税号或其他个人信息),或者通过基于电子邮件的系统。

存储其他类型的敏感信息
列出的许多适用于密码的准则同样适用于其他类型的敏感信息,包括社会保险号和信用卡信息(尽管根据应用程序的不同,其中一些可能需要解密能力)。存储此类信息以多种方式表示它,例如,客户的完整 16 位信用卡号将以高度加密的形式存储,但最后四位数字可能以明文形式存储。作为参考,想想任何显示 XXXX XXXX XXXX 1234 帮助您识别您存储的信用卡。

您可能已经在思考并想知道,鉴于我们公认的使用 SQL 向 H2 数据库填充用户的不切实际的方法,我们如何对密码进行编码? H2 或大多数其他数据库不提供加密方法作为内置数据库功能。

通常,引导过程(使用初始用户和数据填充系统)是通过 SQL 加载和 Java 代码的组合来处理的。根据您的应用程序的复杂性,此过程可能会变得非常复杂。

对于 JBCP 日历应用程序,我们将保留 dataSource() bean 声明,并且 DataSource 是相应 SQL 中代码中的名称,然后添加一些 SQL 将修改密码到他们的哈希值。

PasswordEncoder 方法

Spring Security 中的密码散列由 o.s.s.authentication.encoding.PasswordEncoder 接口的实现封装和定义。密码编码器的简单配置可以通过 AuthenticationManagerBuilder 元素中的 passwordEncoder() 方法进行,如下所示:

    auth
       .jdbcAuthentication()
       .dataSource(dataSource)
       .usersByUsernameQuery(CUSTOM_USERS_BY_USERNAME_QUERY)
       .authoritiesByUsernameQuery(CUSTOM_AUTHORITIES_BY_USERNAME_QUERY)
       .passwordEncoder(passwordEncoder());

您会很高兴地了解到 Spring Security 附带了许多 passwordEncoder 的实现,它们适用于不同的需求和安全要求。

下表提供了开箱即用的实现类及其优势的列表。请注意,所有实现都位于 o.s.s.authentication.encoding 包中:

实现类

说明

哈希值

明文密码编码器

它将密码编码为明文;这是默认设置。

&lt;p>plaintext

Md4PasswordEncoderPasswordEncoder

该编码器使用 MD4 哈希算法。 MD4 散列算法不是安全算法——不推荐使用此编码器。

md4

Md5PasswordEncoderPassword

该编码器使用 MD5 单向编码算法。

ShaPasswordEncoderPasswordEncoder

该编码器使用 SHA 单向编码算法。该编码器可以支持可配置的编码强度级别。

sha-256

LdapShaPasswordEncoder

LdapShaLdapSsha 算法的实现,用于与 LDAP 身份验证存储集成。我们将在第 6 章中了解有关此算法的更多信息, LDAP 目录服务,我们将在其中介绍 LDAP。

{sha}

{ssha}

与 Spring Security 的许多其他领域一样,也可以通过实现 PasswordEncoder 来引用 bean 定义以提供更精确的配置,并允许 PasswordEncoder 通过依赖注入。对于 JBCP 日历应用程序,我们需要使用这个 bean 引用方法来散列新创建用户的密码。

让我们看一下为 JBCP 日历应用程序配置基本密码编码的过程。

配置密码编码

配置基本密码编码涉及两个步骤:在 SQL 脚本执行后对我们加载到数据库中的密码进行哈希处理,并确保 Spring Security 配置为使用 PasswordEncoder

配置 PasswordEncoder 方法

首先,我们将 PasswordEncoder 的实例声明为普通的 Spring bean,如下所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    public ShaPasswordEncoder passwordEncoder(){
       return new ShaPasswordEncoder(256);
    }

您会注意到我们正在使用 SHA-256 PasswordEncoder 实现。这是一种高效的单向加密算法,常用于密码存储。

让 Spring Security 知道 PasswordEncoder 方法

我们需要配置 Spring Security 以引用 PasswordEncoder,以便它可以在用户登录期间对提供的密码进行编码和比较。只需添加一个 passwordEncoder 方法并引用我们在上一步中定义的 bean ID:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth) 
    throws Exception {
    auth
       .jdbcAuthentication()
       .dataSource(dataSource)
       .usersByUsernameQuery(CUSTOM_USERS_BY_USERNAME_QUERY)
       .authoritiesByUsernameQuery(
           CUSTOM_AUTHORITIES_BY_USERNAME_QUERY)
       .passwordEncoder(passwordEncoder())
     ;
    }

如果您此时尝试该应用程序,您会注意到以前有效的登录凭据现在将被拒绝。这是因为存储在数据库中的密码(使用 calendar-users.sql 脚本加载)未存储为与密码编码器匹配的 hash。我们需要将存储的密码更新为散列值。

散列存储的密码

如下图所示,当用户提交密码时,Spring Security 会对提交的密码进行哈希处理,然后将其与数据库中未哈希处理的密码进行比较:

读书笔记《spring-security-third-edition》基于 JDBC 的身份验证

这意味着用户无法登录我们的应用程序。为了解决这个问题,我们将更新在启动时加载的 SQL,以将密码更新为散列值。更新 DataSourceConfig.java 文件,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/DataSourceConfig.java

    @Bean
    public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
       .setName("dataSource")
       .setType(EmbeddedDatabaseType.H2)
       .addScript("/database/h2/calendar-schema.sql")
       .addScript("/database/h2/calendar-data.sql")
       .addScript("/database/h2/calendar-authorities.sql")
       .addScript("/database/h2/calendar-sha256.sql")
       .build();
    }

calendar-sha256.sql 文件只是将现有密码更新为其预期的哈希值,如下所示:

   update calendar_users set password =      
   '0a041b9462caa4a31bac3567e0b6e6fd9100787db2ab433d96f6d178cabfce90' 
   where email = '[email protected]';

我们如何知道将密码更新为什么值?我们提供了 o.s.s.authentication.encoding.Sha256PasswordEncoderMain 来演示如何使用配置的 PasswordEncoder 接口对现有密码进行哈希处理。相关代码如下:

    ShaPasswordEncoder encoder = new ShaPasswordEncoder(256); 
    String encodedPassword = encoder.encodePassword(password, null);

散列新用户的密码

如果我们尝试运行应用程序并创建一个新用户,我们将无法登录。这是因为新创建的用户的密码不会被散列。我们需要更新 DefaultCalendarService 以散列密码。进行以下更新以确保对新创建的用户密码进行哈希处理:

    //src/main/java/com/packtpub/springsecurity/service/DefaultCalendarService.java

    import org.springframework.security.authentication.encoding.PasswordEncoder;
    // other imports omitted
    public class DefaultCalendarService implements CalendarService {
       ...
       private final PasswordEncoder passwordEncoder;
       @Autowired
       public DefaultCalendarService(EventDao eventDao, 
       CalendarUserDao userDao, JdbcOperations jdbcOperations, 
       PasswordEncoder passwordEncoder) {
       ...
       this.passwordEncoder = passwordEncoder;
       }
       ...
       public int createUser(CalendarUser user) {
           String encodedPassword = passwordEncoder.
           encodePassword(user.getPassword(), null);
           user.setPassword(encodedPassword);
           ...
          return userId;
       }
    }

不太安全

继续并启动应用程序。尝试使用 user1 作为密码创建一个新用户。退出应用程序,然后使用 Welcome 页面上的说明打开 H2 控制台并查看所有用户的密码。您是否注意到新创建的用户和 [email protected] 的哈希值是相同的值?我们现在已经找到了另一个用户的密码这一事实有点令人不安。我们将使用一种称为 salting 的技术来解决这个问题。

您的代码现在应该如下所示: calendar04.04-日历

你想用那个密码加点盐吗?如果安全审计员检查数据库中的编码密码,他会发现一些仍然让他担心网站安全的东西。让我们检查一些用户的以下存储的用户名和密码值:

用户名

明文密码

散列密码

[email protected]

admin1

25f43b1486ad95a1398e3eeb3d83bc4010015fcc9bed b35b432e00298d5021f7

[email protected]

用户1

0a041b9462caa4a31bac3567e0b6e6fd9100787db2ab 433d96f6d178cabfce90

这看起来非常安全——加密的密码显然与原始密码没有任何相似之处。审计师会担心什么?如果我们添加一个与 [email protected] 用户密码相同的新用户怎么办?

用户名

明文密码

散列密码

[email protected]

用户1

0a041b9462caa4a31bac3567e0b6e6fd9100787d b2ab433d96f6d178cabfce90

现在,请注意 [email protected] 用户的加密密码与真实用户完全相同!因此,以某种方式获得了读取数据库中加密密码的能力的黑客可以将其已知密码的加密表示与用户帐户的未知密码进行比较,并发现它们是相同的!如果黑客可以访问自动工具来执行此分析,他们可能会在几个小时内破坏用户的帐户。

虽然很难猜出单个密码,但黑客可以提前计算所有哈希值,并将哈希值与原始密码的映射存储起来。然后,找出原始密码就是在恒定时间内通过其哈希值查找密码的问题。这是一种被称为彩虹表的黑客技术。

为加密密码添加另一层安全性的一种常见且有效的方法是加入 salt。盐是第二个明文组件,在执行散列之前与明文密码连接,以确保必须使用两个因素来生成(并因此比较)散列密码值。正确选择的盐可以保证不会有两个密码具有相同的哈希值,从而防止我们的审计员担心的情况,并避免许多常见类型的暴力密码破解技术。

最佳实践盐通常属于以下三类之一:

  • They are algorithmically generated from some pieces of data associated with the user, for example, the timestamp that the user created
  • They are randomly generated and stored in some form
  • They are plaintext or two-way encrypted along with the user's password record

请记住,由于 salt 被添加到明文密码中,它不能被单向加密——应用程序需要能够查找或派生适当的 salt给定用户记录的值,以便计算密码的 hash,并在执行身份验证时将其与存储的用户 hash 进行比较。

在 Spring Security 中使用盐

Spring Security 3.1 提供了一个新的加密模块,该模块包含在 spring-security-core 模块中,并且在 spring-security-crypto 中单独提供。 crypto 模块包含自己的 o.s.s.crypto.password.PasswordEncoder 接口。事实上,使用这个接口是编码密码的首选方法,因为它会使用随机的 salt 对密码进行加盐。在撰写本文时,o.s.s.crypto.password.PasswordEncoder 有以下三种实现:

说明

o.s.s.crypto.bcrypt.BCryptPasswordEncoder

此类使用 bcrypt 散列函数。它支持 salt 以及随着技术的改进而随着时间的推移减慢执行的能力。这有助于防止暴力搜索攻击。

o.s.s.crypto.password.NoOpPasswordEncoder

此类不进行编码(它以明文形式返回密码)。

o.s.s.crypto.password.StandardPasswordEncoder

此类使用具有多次迭代和随机 salt 值的 SHA-256

对于熟悉 Spring Security 3.0 的人来说, salt 曾经使用 o.s.s.authentication.dao.SaltSource。尽管仍受支持,但本书并未演示此机制,因为它不是提供

更新 Spring Security 配置

这可以通过更新 Spring Security 配置来完成。移除旧的 ShaPasswordEncoder 编码器并添加新的 StandardPasswordEncoder 编码器,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    public PasswordEncoder passwordEncoder(){
       return new StandardPasswordEncoder();
    }

迁移现有密码

让我们看一下以下步骤并了解迁移现有密码:

  1. We need to update our existing passwords to use the values produced by the new PasswordEncoder class. If you would like to generate your own passwords, you can use the following code snippet:
        StandardPasswordEncoder encoder = new StandardPasswordEncoder();
        String encodedPassword = encoder.encode("password");
  1. Remove the previously used calendar-sha256.sql file, and add the provided saltedsha256.sql file as follows:
      //src/main/java/com/packtpub/springsecurity/configuration/
      DataSourceConfig.java

      @Bean
      public DataSource dataSource() {
      return new EmbeddedDatabaseBuilder()
         .setName("dataSource")
         .setType(EmbeddedDatabaseType.H2)
         .addScript("/database/h2/calendar-schema.sql")
         .addScript("/database/h2/calendar-data.sql"
         .addScript("/database/h2/calendar-authorities.sql")
         .addScript("/database/h2/calendar-saltedsha256.sql")
         .build();
      }

更新 DefaultCalendarUserService

我们之前定义的 passwordEncoder() 方法足够智能,可以处理新的密码编码器接口。但是,DefaultCalendarUserService 需要更新到新界面。对 DefaultCalendarUserService 类进行以下更新:

    //src/main/java/com/packtpub/springsecurity/service/DefaultCalendarService.java

    import org.springframework.security.authentication.encoding.PasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;

    // other imports omitted

    public class DefaultCalendarService implements CalendarService {
    ...      
    public int createUser(CalendarUser user) {
       String encodedPassword = passwordEncoder.encode(user.getPassword());
       user.setPassword(encodedPassword);
       ...
       return userId;
    }
    }

试用加盐密码

启动应用程序并尝试使用密码 user1 创建另一个用户。使用 H2 控制台比较新用户的密码,并观察它们是否不同。

您的代码现在应该如下所示:calendar04.05-calendar.

Spring Security 现在生成一个随机 salt 并将其与密码组合,然后再对我们的密码进行哈希处理。然后它将随机 salt 添加到明文密码的开头,以便可以检查密码。存储的密码可以总结如下:

    salt = randomsalt()
    hash = hash(salt+originalPassword)
    storedPassword = salt + hash

这是散列新创建的密码的伪代码。

为了验证用户身份,可以从存储的密码中提取 salthash,因为 salthash 都是固定长度。然后,可以将提取的 hash 与新的 hash 进行比较,用提取的 salt 和输入的密码计算:

读书笔记《spring-security-third-edition》基于 JDBC 的身份验证

以下是验证加盐密码的伪代码:

    storedPassword = datasource.lookupPassword(username)
    salt, expectedHash = extractSaltAndHash(storedPassword)
    actualHash = hash(salt+inputedPassword)
    authenticated = (expectedHash == actualHash)

概括

在本章中,我们学习了如何使用 Spring Security 内置的 JDBC 支持。具体来说,我们了解到 Spring Security 为新应用程序提供了默认模式。我们还探索了如何实现 GBAC 以及它如何使管理用户更容易。
我们还学习了如何将 Spring Security 的 JDBC 支持与现有数据库集成,以及如何通过散列密码和使用随机生成的盐来保护我们的密码.

在下一章中,我们将探讨 Spring Data 项目以及如何配置 Spring Security 以使用 object-relational mapping (ORM) 进行连接到 RDBMS 以及文档数据库。