你是否愿意在密码上添加点salt?

如果安全审计人员检查数据库中编码过的密码,在网站安全方面,他可能还会找到一些令其感到担心的地方。让我们查看一下存储的admin和guest用户的用户名和密码值:

用户名

明文密码

加密密码

admin

admin

7b2e9f54cdff413fcde01f330af6896c3cd7e6cd

guest

guest

2ac15cab107096305d0274cd4eb86c74bb35a4b4

这看起来很安全——加密后的密码与初始的密码看不出有任何相似性。但是如果我们添加一个新的用户,而他碰巧和admin用户拥有同样的密码时,又会怎样呢?

用户名

明文密码

加密密码

fakeadmin

admin

7b2e9f54cdff413fcde01f330af6896c3cd7e6cd

现在,注意fakeadmin用户加密过后密码与admin用户完全一致。所以一个黑客如果能够读取到数据库中加密的密码,就能够对已知的密码加密结果和admin账号未知的密码进行对比,并发现它们是一样的。如果黑客能够使用自动化的工具来进行分析,他能够在几个小时内破坏管理员的账号。

【鉴于作者本人使用了一个数据库,它里面的密码使用了完全一致的加密方式,我和工程师团队决定进行一个小的实验并查看明文password的SHA-1加密值。当我们得到password的加密形式并进行数据库查询来查看有多少人使用这个相当不安全的密码。让我们感到非常吃惊的是,这样的人有很多甚至包括组织的一个副总。每个用户都收到了一封邮件提示他们选择难以猜到的密码有什么好处,另外开发人员迅速的使用了一种更安全的密码加密机制。】

请回忆一下我们在第三章中提到的彩虹表技术,恶意的用户如果能够访问到数据库就能使用这个技术来确定用户的密码。这些(以及其它的)黑客技术都是使用了哈希算法的结果都是确定的这一特点——即相同的输入必然会产生相同的输出,所以攻击者如果尝试足够的输入,他们可能会基于已知的输入匹配到未知的输出。

一种通用且高效的方法来添加安全层加密密码就是包含salt(这个单词就是盐的意思,但为了防止直译过来反而不好理解,这里直接使用这个单词——译者注)。Salt是第二个明文组件,它将与前面提到的明文密码一起进行加密以保证使用两个因素来生成(以及进行比较)加密的密码值。选择适当的salt能够保证两个密码不会有相同的编码值,因此可以打消安全审计人员的顾虑,同时能够避免很多常见类型的密码暴力破解技术。

比较好的使用salt的实践不外乎以下的两种类型:

l  使用与用户相关的数据按算法来生成——如,用户创建的时间;

l  随机生成的,并且与用户的密码一起按照某种形式进行存储(明文或者双向加密)。(所谓的双向加密two-way encrypte,指的是加密后还可以进行解密的方式——译者注)

如下图就展现了一个简单的例子,在例子中salt与用户的登录名一致:


 【需要记住的是salt被添加到明文的密码上,所以salt不能进行单向的加密,因为应用要查找用户对应的salt值以完成对用户的认证。】

Spring Security为我们提供了一个接口o.s.s.authentication.dao.SaltSource,它定义了一个方法根据UserDetails来返回salt值,并提供了两个内置的实现:

l  SystemWideSaltSource为所有的密码定义了一个静态的salt值。这与不使用salt的密码相比并没有提高多少安全性;

l  ReflectionSaltSource使用UserDetails对象的一个bean属性得到用户密码的salt值。

鉴于salt值应该能够根据用户数据得到或者与用户数据一起存储,ReflectionSaltSource作为内置的实现被广泛使用。

配置salted密码

与前面配置简单密码加密的练习类似,添加支持salted密码的功能也需要修改启动代码和DaoAuthenticationProvider。我们可以通过查看以下的图来了解salted密码的流程是如何改变启动和认证的,本书的前面章节中我们见过与之类似的图:


 让我们通过配置ReflectionSaltSource实现salt密码,增加密码安全的等级。

声明SaltSource Spring bean

在dogstore-base.xml文件中,增加我们使用的SaltSource实现的bean声明:

  1. <bean class="org.springframework.security.authentication.dao.ReflectionSaltSource" id="saltSource">
  2. <property name="userPropertyToUse" value="username"/>
  3. </bean>
<bean class="org.springframework.security.authentication.dao.ReflectionSaltSource" id="saltSource">
<property name="userPropertyToUse" value="username"/>
</bean>

我们配置salt source使用了username属性,这只是一个暂时的实现,在后面的练习中将会进行修正。你能否想到这为什么不是一个好的salt值吗?

将SaltSource织入到PasswordEncoder中

我们需要将SaltSource织入到PasswordEncoder中,以使得用户在登录时提供的凭证信息能够在与存储值进行比较前,被适当的salted。这通过在dogstore-security.xml文件中添加一个新的声明来完成:

  1. <authentication-manager alias="authenticationManager">
  2. <authentication-provider user-service-ref="jdbcUserService">
  3. <password-encoder ref="passwordEncoder">
  4. <salt-source ref="saltSource"/>
  5. </password-encoder>
  6. </authentication-provider>
  7. </authentication-manager>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="jdbcUserService">
<password-encoder ref="passwordEncoder">
<salt-source ref="saltSource"/>
</password-encoder>
</authentication-provider>
</authentication-manager>

你如果在此时重启应用,你不能登录成功。正如在前面练习中的那样,数据库启动时的密码编码器需要进行修改以包含SaltSource。

增强DatabasePasswordSecurerBean

与UserDetailsService引用类似,我们需要为DatabasePasswordSecurerBean添加对另一个bean的引用(即SaltSource——译者注),这样我们就能够为用户得到合适的密码salt:

  1. public class DatabasePasswordSecurerBean extends JdbcDaoSupport {
  2. @Autowired
  3. private PasswordEncoder passwordEncoder;
  4. @Autowired
  5. private SaltSource saltSource;
  6. @Autowired
  7. private UserDetailsService userDetailsService;
  8. public void secureDatabase() {
  9. getJdbcTemplate().query("select username, password from users",
  10. new RowCallbackHandler(){
  11. @Override
  12. public void processRow(ResultSet rs) throws SQLException {
  13. String username = rs.getString(1);
  14. String password = rs.getString(2);
  15. UserDetails user =
  16. userDetailsService.loadUserByUsername(username);
  17. String encodedPassword =
  18. passwordEncoder.encodePassword(password,
  19. saltSource.getSalt(user));
  20. getJdbcTemplate().update("update users set password = ?
  21. where username = ?",
  22. encodedPassword,
  23. username);
  24. logger.debug("Updating password for username:
  25. "+username+" to: "+encodedPassword);
  26. }
  27. });
  28. }
  29. }
public class DatabasePasswordSecurerBean extends JdbcDaoSupport {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SaltSource saltSource;
@Autowired
private UserDetailsService userDetailsService; 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);

UserDetails user =

userDetailsService.loadUserByUsername(username);

String encodedPassword =

passwordEncoder.encodePassword(password,

saltSource.getSalt(user));

getJdbcTemplate().update("update users set password = ?

where username = ?",

encodedPassword,

username);

logger.debug("Updating password for username:

"+username+" to: "+encodedPassword);

}

});

}

}

回忆一下,SaltSource是要依赖UserDetails对象来生成salt值的。在这里,我们没有数据库行对应UserDetails对象,所以需要请求UserDetailsService(我们的CustomJdbcDaoImpl)的SQL查询以根据用户名查找UserDetails。

到这里,我们能够启动应用并正常登录系统了。如果你添加了一个新用户并使用相同的密码(如admin)到启动的数据库脚本中,你会发现为这个用户生成的密码是不一样的,因为我们使用用户名对密码进行了salt。即使恶意用户能够从数据库中访问密码,这也使得密码更加安全了。但是,你可能会想为什么使用用户名不是最安全的可选salt——我们将会在稍后的一个练习中进行介绍。

增强修改密码功能

我们要完成的另外一个很重要的变化是将修改密码功能也使用密码编码器。这与为CustomJdbcDaoImpl添加bean引用一样简单,并需要changePassword做一些代码修改:

  1. public class CustomJdbcDaoImpl extends JdbcDaoImpl {
  2. @Autowired
  3. private PasswordEncoder passwordEncoder;
  4. @Autowired
  5. private SaltSource saltSource;
  6. public void changePassword(String username, String password) {
  7. UserDetails user = loadUserByUsername(username);
  8. String encodedPassword = passwordEncoder.encodePassword
  9. (password, saltSource.getSalt(user));
  10. getJdbcTemplate().update(
  11. "UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?",
  12. encodedPassword, username);
  13. }
public class CustomJdbcDaoImpl extends JdbcDaoImpl {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SaltSource saltSource;
public void changePassword(String username, String password) {
UserDetails user = loadUserByUsername(username);
String encodedPassword = passwordEncoder.encodePassword
(password, saltSource.getSalt(user));
getJdbcTemplate().update(
"UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?",
encodedPassword, username);
}

这里对PasswordEncoder和SaltSource的使用保证了用户的密码在修改时,被适当的salt。比较奇怪的是,JdbcUserDetailsManager并不支持对PasswordEncoder和SaltSource的使用,所以如果你使用JdbcUserDetailsManager作为基础进行个性化,你需要重写一些代码。

配置自定义的salt source

我们在第一次配置密码salt的时候就提到作为密码salt,username是可行的但并不是一个特别合适的选择。原因在于username作为salt完全在用户的控制下。如果用户能够改变他们的用户名,这就使得恶意的用户可以不断的修改自己的用户名——这样就会重新salt他们的密码——从而可能确定如何构建一个伪造的加密密码。

更安全做法是使用UserDetails的一个属性,这个属性是系统确定的,用户不可见也不可以修改。我们会为UserDetails对象添加一个属性,这个属性在用户创立时被随机设置。这个属性将会作为用户的salt。

扩展数据库scheama

我们需要salt要与用户记录一起保存在数据库中,所以要在默认的Spring Security数据库schema文件security-schema.sql中添加一列:

  1. create table users(
  2. username varchar_ignorecase(50) not null primary key,
  3. password varchar_ignorecase(50) not null,
  4. enabled boolean not null,
  5. salt varchar_ignorecase(25) not null
  6. );
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null,
salt varchar_ignorecase(25) not null
);

接下来,添加启动的salt值到test-users-groups-data.sql脚本中:

  1. insert into users(username, password, enabled, salt) values ('admin','
  2. admin',true,CAST(RAND()*1000000000 AS varchar));
  3. insert into users(username, password, enabled, salt) values ('guest','
  4. guest',true,CAST(RAND()*1000000000 AS varchar));
insert into users(username, password, enabled, salt) values ('admin','
admin',true,CAST(RAND()*1000000000 AS varchar));
insert into users(username, password, enabled, salt) values ('guest','
guest',true,CAST(RAND()*1000000000 AS varchar));

要注意的是,需要用这些新的语句替换原有的insert语句。我们选择的salt值基于随机数生成——你选择任何随机salt都是可以的。

修改CustomJdbcDaoImpl UserDetails service配置

与本章前面讲到的自定义数据库模式中的步骤类似,我们需要修改从数据库中查询用户的配置以保证能够获得添加的“salt”列的数据。我们需要修改dogstore-security.xml文件中CustomJdbcDaoImpl的配置:

  1. <beans:bean id="jdbcUserService"
  2. class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl">
  3. <beans:property name="dataSource" ref="dataSource"/>
  4. <beans:property name="enableGroups" value="true"/>
  5. <beans:property name="enableAuthorities" value="false"/>
  6. <beans:property name="usersByUsernameQuery">
  7. <beans:value>select username,password,enabled,
  8. salt from users where username = ?
  9. </beans:value>
  10. </beans:property>
  11. </beans:bean>
<beans:bean id="jdbcUserService"
class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl">
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="enableGroups" value="true"/>
<beans:property name="enableAuthorities" value="false"/>
<beans:property name="usersByUsernameQuery">
<beans:value>select username,password,enabled,
salt from users where username = ?
</beans:value>
</beans:property>
</beans:bean>

重写基础的UserDetails实现

我们需要一个UserDetails的实现,它包含与用户记录一起存储在数据库中的salt值。对于我们的要求来说,简单重写Spring的标准User类就足够了。要记住的是为salt添加getter个setter方法,这样ReflectionSaltSource密码salter就能够找到正确的属性了。

  1. package com.packtpub.springsecurity.security;
  2. // imports
  3. public class SaltedUser extends User {
  4. private String salt;
  5. public SaltedUser(String username, String password,
  6. boolean enabled,
  7. boolean accountNonExpired, boolean credentialsNonExpired,
  8. boolean accountNonLocked, List<GrantedAuthority>
  9. authorities, String salt) {
  10. super(username, password, enabled,
  11. accountNonExpired, credentialsNonExpired,
  12. accountNonLocked, authorities);
  13. this.salt = salt;
  14. }
  15. public String getSalt() {
  16. return salt;
  17. }
  18. public void setSalt(String salt) {
  19. this.salt = salt;
  20. }
  21. }
package com.packtpub.springsecurity.security;
// imports
public class SaltedUser extends User {
private String salt;
public SaltedUser(String username, String password,
boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, List<GrantedAuthority>
authorities, String salt) {
super(username, password, enabled,
accountNonExpired, credentialsNonExpired,
accountNonLocked, authorities);
this.salt = salt;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
}

我们扩展了UserDetails使其包含一个salt域,如果希望在后台存储用户的额外信息其流程是一样的。扩展UserDetails对象与实现自定义的AuthenticationProvider时经常联合使用。我们将在第六章:高级配置和扩展讲解一个这样的例子。

扩展CustomJdbcDaoImpl功能

我们需要重写JdbcDaoImpl的一些方法,这些方法负责实例化UserDetails对象、设置User的默认值。这发生在从数据库中加载User并复制User到UserDetailsService返回的实例中:

  1. public class CustomJdbcDaoImpl extends JdbcDaoImpl {
  2. public void changePassword(String username, String password) {
  3. getJdbcTemplate().update(
  4. "UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?"
  5. password, username);
  6. }
  7. @Override
  8. protected UserDetails createUserDetails(String username,
  9. UserDetails userFromUserQuery,
  10. List<GrantedAuthority> combinedAuthorities) {
  11. String returnUsername = userFromUserQuery.getUsername();
  12. if (!isUsernameBasedPrimaryKey()) {
  13. returnUsername = username;
  14. }
  15. return new SaltedUser(returnUsername,
  16. userFromUserQuery.getPassword(),userFromUserQuery.isEnabled(),
  17. true, true, true, combinedAuthorities,
  18. ((SaltedUser) userFromUserQuery).getSalt());
  19. }
  20. @Override
  21. protected List<UserDetails> loadUsersByUsername(String username) {
  22. return getJdbcTemplate().
  23. query(getUsersByUsernameQuery(),
  24. new String[] {username},
  25. new RowMapper<UserDetails>() {
  26. public UserDetails mapRow(ResultSet rs, int rowNum)
  27. throws SQLException {
  28. String username = rs.getString(1);
  29. String password = rs.getString(2);
  30. boolean enabled = rs.getBoolean(3);
  31. String salt = rs.getString(4);
  32. return new SaltedUser(username, password,
  33. enabled, true, true, true,
  34. AuthorityUtils.NO_AUTHORITIES, salt);
  35. }
  36. });
  37. }
  38. }
public class CustomJdbcDaoImpl extends JdbcDaoImpl {
public void changePassword(String username, String password) {
getJdbcTemplate().update(
"UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?"
password, username);
}
@Override
protected UserDetails createUserDetails(String username,
UserDetails userFromUserQuery,
List<GrantedAuthority> combinedAuthorities) {
String returnUsername = userFromUserQuery.getUsername();
if (!isUsernameBasedPrimaryKey()) {
returnUsername = username;
}
return new SaltedUser(returnUsername,
userFromUserQuery.getPassword(),userFromUserQuery.isEnabled(),
true, true, true, combinedAuthorities,
((SaltedUser) userFromUserQuery).getSalt());
}
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().
query(getUsersByUsernameQuery(),
new String[] {username},
new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum)
throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
String salt = rs.getString(4);
return new SaltedUser(username, password,
enabled, true, true, true,
AuthorityUtils.NO_AUTHORITIES, salt);
}
});
}
}

createUserDetails和loadUsersByUsername重写了父类的方法——与父类不同的地方在代码列表中已经着重强调出来了。添加了这些变化,你可以重启应用并拥有了更安全、随机的salt密码。你可能会愿意加一些日志和实验,以查看应用运行期间和启动时用户数据加载时的加密数据变化。

要记住的是,尽管在这个例子中说明的是为UserDetails添加一个简单域的实现,这种方式可以作为基础来实现高度个性化的UserDetails对象以满足应用的业务需要。对于JBCP Pets来说,审计人员会对数据库中的安全密码感到很满意——一项任务被完美完成。

《Spring Security3》第四章第三部分翻译下(密码加salt)的更多相关文章

  1. Spring实战第四章学习笔记————面向切面的Spring

    Spring实战第四章学习笔记----面向切面的Spring 什么是面向切面的编程 我们把影响应用多处的功能描述为横切关注点.比如安全就是一个横切关注点,应用中许多方法都会涉及安全规则.而切面可以帮我 ...

  2. 使用Spring Security3的四种方法概述

    使用Spring Security3的四种方法概述 那么在Spring Security3的使用中,有4种方法: 一种是全部利用配置文件,将用户.权限.资源(url)硬编码在xml文件中,已经实现过, ...

  3. 《学习OpenCV》练习题第四章第三题b

    #include <highgui.h> #include <cv.h> #include "opencv_libs.h" /* *<学习OpenCV ...

  4. 《学习OpenCV》练习题第四章第三题a

    #include <highgui.h> #include <cv.h> #include "opencv_libs.h" #pragma comment ...

  5. python全栈开发中级班全程笔记(第二模块、第四章(三、re 正则表达式))

    python全栈开发笔记第二模块   第四章 :常用模块(第三部分) 一.正则表达式的作用与方法 正则表达式是什么呢?一个问题带来正则表达式的重要性和作用      有一个需求 : 从文件中读取所有联 ...

  6. 一起来学Spring Cloud | 第四章:服务消费者 ( Feign )

    上一章节,讲解了SpringCloud如何通过RestTemplate+Ribbon去负载均衡消费服务,本章主要讲述如何通过Feign去消费服务. 一.Feign 简介: Feign是一个便利的res ...

  7. “全栈2019”Java第三十四章:可变参数列表

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  8. Spring Security3十五日研究(转载)

    前言 南朝<述异记>中记载,晋王质上山砍柴,见二童子下棋,未看完,斧柄已烂,下山回村,闻同代人都去世了,自已还未变老.    因此发出“山中方一日,世上几千年” 的慨叹.原文寥寥几笔,读来 ...

  9. 一起来学Spring Cloud | 第六章:服务网关 ( Zuul)

    本章节,我们讲解springcloud重要组件:微服务网关Zuul.如果有同学从第一章看到本章的,会发现我们已经讲解了大部分微服务常用的基本组件. 已经讲解过的: 一起来学Spring Cloud | ...

随机推荐

  1. elasticsearch最全详细使用教程:搜索详解

    一.搜索API 1. 搜索API 端点地址从索引tweet里面搜索字段user为kimchy的记录 GET /twitter/_search?q=user:kimchy从索引tweet,user里面搜 ...

  2. java数字金额转化为中文金额

    public static String digitUppercase(double n){String fraction[] = {"角", "分"};Str ...

  3. http post get 同步异步

    下面首先介绍一下一些基本的概念---同步请求,异步请求,GET请求,POST请求. 1.同步请求从因特网请求数据,一旦发送同步请求,程序将停止用户交互,直至服务器返回数据完成,才可以进行下一步操作.也 ...

  4. IE(IE6/IE7/IE8)支持HTML5标签--20150216

    让IE(ie6/ie7/ie8)支持HTML5元素,我们需要在HTML头部添加以下JavaScript,这是一个简单的document.createElement声明,利用条件注释针对IE来调用这个j ...

  5. LeetCode 最长连续递增序列

    给定一个未经排序的整数数组,找到最长且连续的的递增序列. 示例 1: 输入: [1,3,5,4,7] 输出: 3 解释: 最长连续递增序列是 [1,3,5], 长度为3. 尽管 [1,3,5,7] 也 ...

  6. ECshop二次开发 ECSHOP首页显示积分商城里的商品

    以ECSHOP2.7.2官方默认模板为基础 1).首先打开 index.php 文件,在最末尾增加下面函数,注意千万不要写到 “?>” 的外面去,要加在“?>”的前面,加以下代码: /** ...

  7. redux form

    纯粹使用react进行表单校验: class MyForm extends React.Component{ constructor(props){ super(props) this.onAddrC ...

  8. C语言之extern、const、volatile

    extern: extern修饰变量,声明该变量为外部文件的全局变量.若使用外部全局变量,必须用extern声明. extern修饰函数,声明该函数为外部函数.extern修饰的函数形参必须与原函数一 ...

  9. perl学习之:肯定匹配和否定匹配

    tr/ / / 替换操作符不支持正则表达式 也不具备双引号替换能力m/ /  s/ / / 都支持正则表达式,并且可以提供或限制双引号替换能力 $string = "25abc8" ...

  10. Django ORM字段和字段参数

    Object Relational Mapping(ORM) ORM介绍 ORM概念 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据 ...