配置安全的密码
我们回忆第一章:一个不安全应用的剖析 中,审计人员认为密码以明文形式进行存储是最高优先级的安全风险。实际上,在任何安全系统中,密码安全都是保证已经经过认证的安全实体是真实可靠的重要方面。安全系统的设计人员必须保证密码存储时,任何恶意的用户想要进行破解都是非常困难的。
在数据库存储时,需要遵守以下的准则:
l 密码不能以明文的形式进行存储(简单文本);
l 用户提供的密码必须与数据库存储的密码进行比较;
l 密码不能应用户的请求提供(即使用户忘记了密码)。
对于大多数应用来说,为了满足以上要求最适合的方式是对密码进行单向的编码或加密。单向编码提供了安全和唯一的特性,这对于正确的认证用户非常重要,它能够保证密码一旦被加密就不能再被解密了。
在大多数的安全应用设计中,一般没有必要和实际要求根据用户的请求检索用户的密码,因为如果没有附加的安全认证,提供用户的实际密码会有很大的安全风险。作为替代方案,大多数的应用为用户提供了重设密码的功能,要么需要提供额外的认证信息(如社会保险号码、生日、缴税 ID 或其它个人信息),要么通过一个基于 email 的系统。
【存储其它类型的敏感信息:以下的准则可以应用于密码以及其它类型的敏感信息,包括社会保险号以及信用卡信息(尽管基于应用的不同,它们中的一些需要解密的能力)。比较常见的是数据库以多种形式来存储这些敏感信息,比如用户 16 个数字的信用卡数字可以用一种高度加密的形式存储,但是最后的四位数字以明文的形式存储(作为佐证,可以回忆一些商业站点会显示 XXXX XXXX XXXX 1234 来帮助你分别已存储的信用卡)。】
基于我们(实际上并不太符合现实)使用 SQL 来访问 HSQL 数据库中用户的环境,你可能已经在思考如何对密码进行编码。 HSQL 以及其它大多数的数据库并没有提供加密方法作为数据库的内置功能。
一般来说, bootstrap 过程(为系统添加初始的用户和数据)会联合使用一些 SQL 加载和 Java 代码。根据应用的复杂性,这个过程也可能会变得很复杂。
对于 JBCP Pets 应用来说,我们将会继续使用嵌入式数据库声明和对应的 SQL ,并且会添加一点 Java 代码在初始化加载后执行来加密数据库中的所有密码。为了使得密码加密能够正常工作,两个过程必须同步的使用密码加密以确保密码能够被一致的处理和校验。
在 Spring Security 中,密码加密已经进行了封装,通过 o.s.s.authentication.encoding.PasswordEncoder 接口的实现类来定义。通过使用 <authentication-provider> 元素里的 <password-encoder> 声明我们能够很容易地配置密码编码:
- < authentication-manager alias = "authenticationManager" >
- < authentication-provider user-service-ref = "jdbcUserService" >
- < password-encoder hash = "sha" />
- </ authentication-provider >
- </ authentication-manager >
你可能会很高兴的了解到 Spring Security 已经提供了一系列 PasswordEncoder 的实现,它们可以用于不同的需求和安全需要。要使用哪个实现可以通过 <password-encoder> 元素的 hash 属性来指定。
以下的列表列出了内置的实现类以及它们的优点。这些实现类都在 o.s.s.authentication.
Encoding 包下。
实现类 | 描述 | hash 值 |
PlaintextPasswordEncoder | 以明文的形式编码。 DaoAuthenticationProvider 默认的密码编码器。 | plaintext |
Md4PasswordEncoder | PasswordEncoder 使用 MD4 hash 算法。 MD4 并不是一个安全的算法——不推荐使用这个编码器 | md4 |
Md5PasswordEncoder | PasswordEncoder 使用 MD5 的单向编码算法。 | md5 |
ShaPasswordEncoder | PasswordEncoder 使用 SHA 单向加密算法。这个编码器支持配置密码的强度级别。 | sha sha-256 |
LdapShaPasswordEncoder | 在集成 LDAP 认证存储时使用,实现了 LDAP SHA 和 LDAP SSHA 算法。我们将会在第九章: LDAP 目录服务 讲述 LDAP 时,学习更多关于这个算法的知识。 | {sha} {ssha} |
与 Spring Security 其他领域一样,可以引用一个 PasswordEncoder 的实现类以提供更精确的配置,并允许 PasswordEncoder 通过依赖注入织入到其它的 bean 中。对于 JBCP Pets 来说,我们需要使用这个 bean 引用的方法来编码用户的初始数据。
让我们了解一下为 JBCP Pet 应用配置基本密码编码的过程。
配置密码编码
配置基本的密码编码涉及到两个地方——在我们的 SQL 脚本执行后,加密载入数据库中数据的密码,并确保 DaoAuthenticationProvider 被配置成使用 PasswordEncoder 。
配置 PasswordEncoder
首先,作为通常的 Spring bean ,声明一个 PasswordEncoder 的实例
- < bean class ="org.springframework.security.authentication.
- encoding.ShaPasswordEncoder" id = "passwordEncoder" />
你会发现我们使用了 SHA-1 的 PasswordEncoder 实现。这是一个高效的单向加密算法,在密码存储中经常用到。
配置 AuthenticationProvider
我们需要配置 DaoAuthenticationProvider 来持有一个对 PasswordEncoder 的引用,这样它就可以在用户登录时,编码并比较用户提供的密码。添加 <password-encoder> 声明并指向我们在前面定义的 bean 的 ID :
- < authentication-manager alias = "authenticationManager" >
- < authentication-provider user-service-ref = "jdbcUserService" >
- < password-encoder ref = "passwordEncoder" />
- </ authentication-provider >
- </ authentication-manager >
如果在此时重启应用,并尝试登录,你会发现前面合法的登录凭证现在都会被拒绝。这是因为数据库中存储的密码(在启动时通过 test-users-groups-data.sql 脚本加载的)并没有以加密的形式存储。我们需要用一些 java 代码对启动数据进行后置的处理。
编写数据库启动的密码编码器
我们对 SQL 加载的数据进行编码的方式是使用了 Spring bean 的初始化方法,它将在 embedded-database bean 实例化完成后执行。这个 bean , com.packtpub.springsecurity.security.DatabasePasswordSecurerBean 很简单:
- public class DatabasePasswordSecurerBean extends JdbcDaoSupport {
- @Autowired
- private PasswordEncoder passwordEncoder;
- public void secureDatabase() {
- getJdbcTemplate().query("select username, password from users" ,
- new RowCallbackHandler(){
- @Override
- public void processRow(ResultSet rs) throws SQLException {
- String username = rs.getString(1 );
- String password = rs.getString(2 );
- String encodedPassword =
- passwordEncoder.encodePassword(password, null );
- getJdbcTemplate().update("update users set password = ?
- where username = ?", encodedPassword,username);
- logger.debug("Updating password for username:
- "+username+" to: "+encodedPassword);
- }
- });
- }
- }
代码使用了 JdbcTemplate 功能来遍历所有的数据库中所有的用户并使用注入的 PasswordEncoder 引用对密码进行编码。每一个密码都进行了更新。
配置启动的密码编码
我们需要配置这个 Spring bean 的声明以使其在 web 应用启动时及 <embedded-database> 初始化后再进行该类的初始化。 Spring bean 的依赖跟踪机制保证 DatabasePasswordSecurerBean 能够在合适的时机执行:
- < bean class ="com.packtpub.springsecurity.security.
- DatabasePasswordSecurerBean"
- init-method = "secureDatabase" depends-on = "dataSource" >
- < property name = "dataSource" ref = "dataSource" />
- </ bean >
如果你此时重启 JBCP Pets 应用,你会发现数据库中的密码已经进行了编码,登录功能可以正常使用了。