《Spring Security3》第四章第三部分翻譯下(密碼加salt)
你是否願意在密碼上添加點salt?
如果安全審計人員檢查數據庫中編碼過的密碼,在網站安全方面,他可能還會找到一些令其感到擔心的地方。讓我們查看一下存儲的admin和guest用戶的用戶名和密碼值:
用戶名 | 明文密碼 | 加密密碼 |
admin | admin | 7b2e9f54cdff413fcde01f330af6896c3cd7e6cd |
guest | guest | 2ac15cab107096305d0274cd4eb86c74bb35a4b4 |
這看起來很安全——加密後的密碼與初始的密碼看不出有任何相似性。但是如果我們添加一個新的用戶,而他碰巧和admin用戶擁有同樣的密碼時,又會怎樣呢?
用戶名 | 明文密碼 | 加密密碼 |
fakeadmin | admin | 7b2e9f54cdff413fcde01f330af6896c3cd7e6cd |
現在,註意fakeadmin用戶加密過後密碼與admin用戶完全一致。所以一個黑客如果能夠讀取到數據庫中加密的密碼,就能夠對已知的密碼加密結果和admin賬號未知的密碼進行對比,並發現它們是一樣的。如果黑客能夠使用自動化的工具來進行分析,他能夠在幾個小時內破壞管理員的賬號。
【鑒於作者本人使用了一個數據庫,它裏面的密碼使用了完全一致的加密方式,我和工程師團隊決定進行一個小的實驗並查看明文password的SHA-1加密值。當我們得到password的加密形式並進行數據庫查詢來查看有多少人使用這個相當不安全的密碼。讓我們感到非常吃驚的是,這樣的人有很多甚至包括組織的一個副總。每個用戶都收到了一封郵件提示他們選擇難以猜到的密碼有什麽好處,另外開發人員迅速的使用了一種更安全的密碼加密機制。】
請回憶一下我們在第三章中提到的彩虹表技術,惡意的用戶如果能夠訪問到數據庫就能使用這個技術來確定用戶的密碼。這些(以及其它的)黑客技術都是使用了哈希算法的結果都是確定的這一特點——即相同的輸入必然會產生相同的輸出,所以攻擊者如果嘗試足夠的輸入,他們可能會基於已知的輸入匹配到未知的輸出。
一種通用且高效的方法來添加安全層加密密碼就是包含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聲明:
Xml代碼
- <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文件中添加一個新的聲明來完成:
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。
增強DatabasePasswordSecurerBean
與UserDetailsService引用類似,我們需要為DatabasePasswordSecurerBean添加對另一個bean的引用(即SaltSource——譯者註),這樣我們就能夠為用戶得到合適的密碼salt:
Java代碼
- 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做一些代碼修改:
Java代碼
- 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中添加一列:
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腳本中:
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都是可以的。
修改CustomJdbcDaoImpl UserDetails service配置
與本章前面講到的自定義數據庫模式中的步驟類似,我們需要修改從數據庫中查詢用戶的配置以保證能夠獲得添加的“salt”列的數據。我們需要修改dogstore-security.xml文件中CustomJdbcDaoImpl的配置:
Xml代碼
- <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就能夠找到正確的屬性了。
Java代碼
- 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返回的實例中:
Java代碼
- 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)