你是否願意在密碼上添加點salt?java
若是安全審計人員檢查數據庫中編碼過的密碼,在網站安全方面,他可能還會找到一些令其感到擔憂的地方。讓咱們查看一下存儲的admin和guest用戶的用戶名和密碼值:算法
這看起來很安全——加密後的密碼與初始的密碼看不出有任何類似性。可是若是咱們添加一個新的用戶,而他碰巧和admin用戶擁有一樣的密碼時,又會怎樣呢?spring
如今,注意fakeadmin用戶加密事後密碼與admin用戶徹底一致。因此一個黑客若是可以讀取到數據庫中加密的密碼,就可以對已知的密碼加密結果和admin帳號未知的密碼進行對比,並發現它們是同樣的。若是黑客可以使用自動化的工具來進行分析,他可以在幾個小時內破壞管理員的帳號。sql
【鑑於做者本人使用了一個數據庫,它裏面的密碼使用了徹底一致的加密方式,我和工程師團隊決定進行一個小的實驗並查看明文password的SHA-1加密值。當咱們獲得password的加密形式並進行數據庫查詢來查看有多少人使用這個至關不安全的密碼。讓咱們感到很是吃驚的是,這樣的人有不少甚至包括組織的一個副總。每一個用戶都收到了一封郵件提示他們選擇難以猜到的密碼有什麼好處,另外開發人員迅速的使用了一種更安全的密碼加密機制。】數據庫
請回憶一下咱們在第三章中提到的彩虹表技術,惡意的用戶若是可以訪問到數據庫就能使用這個技術來肯定用戶的密碼。這些(以及其它的)黑客技術都是使用了哈希算法的結果都是肯定的這一特色——即相同的輸入必然會產生相同的輸出,因此攻擊者若是嘗試足夠的輸入,他們可能會基於已知的輸入匹配到未知的輸出。安全
一種通用且高效的方法來添加安全層加密密碼就是包含salt(這個單詞就是鹽的意思,但爲了防止直譯過來反而很差理解,這裏直接使用這個單詞——譯者注)。Salt是第二個明文組件,它將與前面提到的明文密碼一塊兒進行加密以保證使用兩個因素來生成(以及進行比較)加密的密碼值。選擇適當的salt可以保證兩個密碼不會有相同的編碼值,所以能夠打消安全審計人員的顧慮,同時可以避免不少常見類型的密碼暴力破解技術。併發
比較好的使用salt的實踐不外乎如下的兩種類型:app
使用與用戶相關的數據按算法來生成——如,用戶建立的時間;ide
隨機生成的,而且與用戶的密碼一塊兒按照某種形式進行存儲(明文或者雙向加密)。(所謂的雙向加密two-way encrypte,指的是加密後還能夠進行解密的方式——譯者注)工具
以下圖就展示了一個簡單的例子,在例子中salt與用戶的登陸名一致:
【須要記住的是salt被添加到明文的密碼上,因此salt不能進行單向的加密,由於應用要查找用戶對應的salt值以完成對用戶的認證。】
Spring Security爲咱們提供了一個接口o.s.s.authentication.dao.SaltSource,它定義了一個方法根據UserDetails來返回salt值,並提供了兩個內置的實現:
SystemWideSaltSource爲全部的密碼定義了一個靜態的salt值。這與不使用salt的密碼相比並無提升多少安全性;
ReflectionSaltSource使用UserDetails對象的一個bean屬性獲得用戶密碼的salt值。鑑於salt值應該可以根據用戶數據獲得或者與用戶數據一塊兒存儲,ReflectionSaltSource做爲內置的實現被普遍使用。
配置salted密碼
與前面配置簡單密碼加密的練習相似,添加支持salted密碼的功能也須要修改啓動代碼和DaoAuthenticationProvider。咱們能夠經過查看如下的圖來瞭解salted密碼的流程是如何改變啓動和認證的,本書的前面章節中咱們見過與之相似的圖:
讓咱們經過配置ReflectionSaltSource實現salt密碼,增長密碼安全的等級。
在dogstore-base.xml文件中,增長咱們使用的SaltSource實現的bean聲明:
<bean class="org.springframework.security.authentication.dao.ReflectionSaltSource" id="saltSource"> <property name="userPropertyToUse" value="username"/> </bean>
咱們配置salt source使用了username屬性,這只是一個暫時的實現,在後面的練習中將會進行修正。你可否想到這爲何不是一個好的salt值嗎?
咱們須要將SaltSource織入到PasswordEncoder中,以使得用戶在登陸時提供的憑證信息可以在與存儲值進行比較前,被適當的salted。這經過在dogstore-security.xml文件中添加一個新的聲明來完成:
<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。
與UserDetailsService引用相似,咱們須要爲DatabasePasswordSecurerBean添加對另外一個bean的引用(即SaltSource——譯者注),這樣咱們就可以爲用戶獲得合適的密碼salt:
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作一些代碼修改:
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的時候就提到做爲密碼salt,username是可行的但並非一個特別合適的選擇。緣由在於username做爲salt徹底在用戶的控制下。若是用戶可以改變他們的用戶名,這就使得惡意的用戶能夠不斷的修改本身的用戶名——這樣就會從新salt他們的密碼——從而可能肯定如何構建一個僞造的加密密碼。
更安全作法是使用UserDetails的一個屬性,這個屬性是系統肯定的,用戶不可見也不能夠修改。咱們會爲UserDetails對象添加一個屬性,這個屬性在用戶創立時被隨機設置。這個屬性將會做爲用戶的salt。
咱們須要salt要與用戶記錄一塊兒保存在數據庫中,因此要在默認的Spring Security數據庫schema文件security-schema.sql中添加一列:
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腳本中:
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都是能夠的。
與本章前面講到的自定義數據庫模式中的步驟相似,咱們須要修改從數據庫中查詢用戶的配置以保證可以得到添加的「salt」列的數據。咱們須要修改dogstore-security.xml文件中CustomJdbcDaoImpl的配置:
<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的實現,它包含與用戶記錄一塊兒存儲在數據庫中的salt值。對於咱們的要求來講,簡單重寫Spring的標準User類就足夠了。要記住的是爲salt添加getter個setter方法,這樣ReflectionSaltSource密碼salter就可以找到正確的屬性了。
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時常常聯合使用。咱們將在第六章:高級配置和擴展講解一個這樣的例子。
咱們須要重寫JdbcDaoImpl的一些方法,這些方法負責實例化UserDetails對象、設置User的默認值。這發生在從數據庫中加載User並複製User到UserDetailsService返回的實例中:
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來講,審計人員會對數據庫中的安全密碼感到很滿意——一項任務被完美完成。