apahce shiro:1.6.0
密碼存儲,應該加密/生成密碼摘要存儲,而不是存儲明文密碼。
1、編碼/解碼
Shiro 提供了 base64和 16進制字符串編碼/解碼的API支持, 方便一些編碼解碼操作。
String str = "hello"; #base64 String base64Encoded = Base64.encodeToString(str.getBytes()); String str2 = Base64.decodeToString(base64Encoded); #十六進制 String base64Encoded = Hex.encodeToString(str.getBytes()); String str2 = new String(Hex.decode(base64Encoded.getBytes()));
還有一個可能經常用到的類CodecSupport,提供了toBytes(str, "utf-8") / toString(bytes, "utf-8")用於在 byte數組/String之間轉換。
2、散列算法
散列算法一般用於生成數據的摘要信息,是一種不可逆的算法,一般適合存儲密碼之類的數據,常見的散列算法如 MD5、SHA 等。
2.1 MD5散列舉例
String str = "admin"; String salt = ""; String md5 = new Md5Hash(str, salt).toString();//21232f297a57a5a743894a0e4a801fc3
可以到一些 md5 解密網站很容易的通過散列值得到密碼“admin”,即如果直接對密碼進行散列相對來說破解更容易;
此時我們可以加一些只有系統知道的干擾數據, 如用戶名和 ID (即鹽);這樣散列的對象是“密碼+用戶名+ID”,這樣生成的散列值相對來說更難破解:
String str = "admin"; String salt = "123"; String md5 = new Md5Hash(str, salt).toString();//d829b843a6550a947e82f2f38ed6b7a7
另外散列時還可以指定散列次數,如2次表示:md5(md5(str))
String str = "admin"; String salt = "123"; String md5 = new Md5Hash(str, salt, 2).toString();//6bdae6366c1e46d541eb0ca9547d974c
2.2 使用SHA算法(SHA1、SHA256、SHA512)生成相應的散列數據
String str = "admin"; String salt = "123"; String sha1 = new Sha1Hash(str, salt).toString();//28dca2a7b33b7413ad3bce1d58c26dd679c799f1 String sha256 = new Sha256Hash(str, salt).toString();//82a79f11b4acb52a642ef7e339dfce4aa92ff65ed2e7ab702d798dbe10eca0b8 String sha512 = new Sha512Hash(str, salt).toString();//cefbd13986ef4b4c6d57e681da43f7abc076d4d6236df728c1b57519763edd305ee8d6d3c94d5d853dbdc36c1a3169c5e7c4d8bccbf48fb31a6e0eb7758a9f8f
2.3 Shiro 還提供了通用的散列支持
通過調用 SimpleHash 時指定散列算法,其內部使用了 Java 的 MessageDigest 實現
String str = "admin"; String salt = "123"; //內部使用 MessageDigest String simpleHash = new SimpleHash("SHA-1", str, salt).toString();
2.4 為了方便使用,Shiro 提供了 HashService,默認提供了 DefaultHashService 實現
DefaultHashService hashService = new DefaultHashService(); //默認算法 SHA-512 hashService.setHashAlgorithmName("SHA-512"); //通過 hashAlgorithmName 屬性修改算法 hashService.setPrivateSalt(new SimpleByteSource("123")); //通過 privateSalt 設置一個私鹽,其在散列時自動與用戶傳入的公鹽混合產生一個新鹽,默認無 hashService.setGeneratePublicSalt(true);//是否生成公鹽,默認 false hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());//用於生成公鹽。默認就這個 hashService.setHashIterations(1); //生成 Hash 值的迭代次數 #構建一個 HashRequest,傳入算法、數據、公鹽、迭代次數 HashRequest request = new HashRequest.Builder() .setAlgorithmName("MD5").setSource(ByteSource.Util.bytes("hello")) .setSalt(ByteSource.Util.bytes("123")).setIterations(2).build(); String hex = hashService.computeHash(request).toHex();
2.5 SecureRandomNumberGenerator 用於生成一個隨機數
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); randomNumberGenerator.setSeed("123".getBytes()); String hex = randomNumberGenerator.nextBytes().toHex();
3、加密/解密
Shiro 還提供對稱式加密/解密算法的支持,如 AES、Blowfish.
AesCipherService aesCipherService = new AesCipherService(); aesCipherService.setKeySize(128); //設置 key 長度 //生成 key Key key = aesCipherService.generateNewKey(); String text = "hello"; //加密 String encrptText = aesCipherService.encrypt(text.getBytes(), key.getEncoded()).toHex(); //解密 String text2 = new String(aesCipherService.decrypt(Hex.decode(encrptText), key.getEncoded()).getBytes()); Assert.assertEquals(text, text2);
4、PasswordService
Shiro 提供了PasswordService用於提供加密密碼服務。
public interface PasswordService { //輸入明文密碼得到密文密碼 String encryptPassword(Object plaintextPassword) throws IllegalArgumentException; }
DefaultPasswordService配合PasswordMatcher 實現簡單的密碼加密與驗證服務。實際使用:在Realm注入一個 passwordService 來加密密碼用於后面的驗證匹配或者在用戶模塊里新增用戶和修改密碼時可以使用passwordService 加密密碼並存到數據庫.

package com.github.zhangkaitao.shiro.chapter5.hash.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; /** * <p>User: Zhang Kaitao * <p>Date: 14-1-27 * <p>Version: 1.0 */ public class MyRealm extends AuthorizingRealm { private PasswordService passwordService; public void setPasswordService(PasswordService passwordService) { this.passwordService = passwordService; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { return new SimpleAuthenticationInfo( "wu", passwordService.encryptPassword("123"), getName()); } }
5、CredentialsMatcher
Shiro 提供了CredentialsMatcher用於提供驗證密碼服務。
public interface CredentialsMatcher { //匹配用戶輸入的 token 的憑證(未加密)與系統提供的憑證(已加密) boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info); }
Shiro提供了CredentialsMatcher的散列實現HashedCredentialsMatcher, PasswordMatcher只用於密碼驗證且可以提供自己的鹽, 而不是隨機生成鹽,所以生成密碼散列值的算法需要自己寫。
(1)生成密碼散列值
此處我們使用 MD5 算法,"密碼+鹽(用戶名+隨機數)"的方式生成散列值
String algorithmName = "md5"; String username = "liu"; String password = "123"; String salt1 = username; String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex(); int hashIterations = 2; SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations);
如果要寫用戶模塊,需要在新增用戶/重置密碼時使用如上算法保存密碼,將生成的密碼及salt2 存入數據庫(因為我們的散列算法是:md5(md5(密碼+username+salt2)))。
(2)生成 Realm
此處就是把步驟1中生成的相應數據組裝為SimpleAuthenticationInfo,通過SimpleAuthenticationInfo的credentialsSalt設置鹽,HashedCredentialsMatcher會自動識別這個鹽

package com.github.zhangkaitao.shiro.chapter5.hash.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; /** * <p>User: Zhang Kaitao * <p>Date: 14-1-27 * <p>Version: 1.0 */ public class MyRealm2 extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = "liu"; //用戶名及salt1 String salt2 = "0072273a5d87322163795118fdd7c45e"; String password = "be320beca57748ab9632c4121ccac0db"; //加密后的密碼 SimpleAuthenticationInfo ai = new SimpleAuthenticationInfo(username, password, getName()); ai.setCredentialsSalt(ByteSource.Util.bytes(username+salt2)); //鹽是用戶名+隨機數 return ai; } }
(3)密碼重試次數限制
如在 1 個小時內密碼最多重試 5 次,如果嘗試次數超過 5 次就鎖定 1 小時,1 小時后可再次重試,如果還是重試失敗,可以鎖定如 1 天,以此類推,防止密碼被暴力破解。我們通過繼承 HashedCredentialsMatcher,且使用 Ehcache 記錄重試次數和超時時間。

package com.github.zhangkaitao.shiro.chapter5.hash.credentials; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import javax.security.auth.login.AccountLockedException; import java.util.concurrent.atomic.AtomicInteger; /** * <p>User: Zhang Kaitao * <p>Date: 14-1-28 * <p>Version: 1.0 */ public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Ehcache passwordRetryCache; public RetryLimitHashedCredentialsMatcher() { CacheManager cacheManager = CacheManager.newInstance(CacheManager.class.getClassLoader().getResource("ehcache.xml")); passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); //retry count + 1 Element element = passwordRetryCache.get(username); if(element == null) { element = new Element(username , new AtomicInteger(0)); passwordRetryCache.put(element); } AtomicInteger retryCount = (AtomicInteger)element.getObjectValue(); if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); } return matches; } }
如上代碼邏輯比較簡單, 即如果密碼輸入正確清除 cache 中的記錄; 否則 cache 中的重試次數+1,如果超出 5 次那么拋出異常表示超出重試次數了。