- 密码存储应加密或生成摘要存储。
编码、解码
- Shiro提供了base64和16进制对字符串进行编码、解码。其内部的一些数据的存储和表示也都使用了base64和16进制的字符串。
- base64:网络上最常用的用于传输8bit字节码的编码方式之一,可用于在HTTP环境下传递较长的标识信息,起到简单的加密作用。主要是基于64个可打印字符来表示二进制数据,即编码过程是从二进制到字符的过程。
String str = "hello";
String base64Encoded = Base64.encodeToString(str.getBytes()); //编码:二进制->字符
String str2 = Base64.decodeToString(base64Encoded); //解码
Assert.assertEquals(str, str2);
- 16进制
String str = "hello";
String hexEncoded = Hex.encodeToString(str.getBytes()); //编码
String str2 = new String(Hex.decode(hexEncoded.getBytes())); //解码
Assert.assertEquals(str, str2);
- CodecSupport类:提供toBytes(str,”utf-8”)、toString(bytes,”utf-8”),进行String和byte数组之间的额转换。
散列算法
- 散列算法一般用于生成数据的摘要信息,不可逆。
- 常见的有:MD5、SHA等。
- 一般进行散列时最好提供一个salt,比如一些只有系统知道的干扰数据,如,用户名和id,这样生成的散列值更难破解。
- 因此用户注册和修改密码时,系统应将密码和盐一起保存到数据库。
String str = "hello";
String salt = "123";
String md5 = new Md5Hash(str, salt).toString();//还可以是toBase64()/toHex();
String sha1 = new Sha256Hash(str, salt).toString();//还有SHA1、SHA512、SHA256、SHA384...
- 还可以指定散列次数
//散列两次
md5(md5(str));
//相当于
new MD5Hash(str,salt,2).toString();
对称加密
- AES算法:下一代加密算法标准,速度快,安全级别高,支持128/192/256/512位秘钥的加密。
AesCipherService aesCipherService=new AesCipherService();
aesCipherService.setKeySize(128); //设置key长度;不影响加密后长度
Key key=aesCipherService.generateNewKey(); //生成key
String source="hello";
String encryptText=aesCipherService.encrypt(source.getBytes(),key.getEncoded())
.toHex(); //加密,参数是两个byte数组
String source2=new String(aesCipherService.decrypt(Hex.decode(encryptText),
key.getEncoded()).getBytes()); //解密,参数是两个byte数组
- crypt:C语言加密函数名。
代码实例:shiroHelloWorld/test/AllTest
Blowfish算法:布鲁斯·施奈尔发明的区块加密算法。
BlowfishCipherService blowfishCipherService = new BlowfishCipherService();
blowfishCipherService.setKeySize(128);
//生成key
Key key = blowfishCipherService.generateNewKey();
String text = "hello";
//加密
String encrptText = blowfishCipherService.encrypt(text.getBytes(), key.getEncoded()).toHex();
//解密
String text2 = new String(blowfishCipherService.decrypt(Hex.decode(encrptText), key.getEncoded()).getBytes());
Assert.assertEquals(text, text2);
- DefaultBlockCipherService:对称加密通用支持器
//对称加密,使用Java的JCA(javax.crypto.Cipher)加密API,常见的如 'AES', 'Blowfish'
DefaultBlockCipherService cipherService = new DefaultBlockCipherService("AES");
cipherService.setKeySize(128);
//生成key
Key key = cipherService.generateNewKey();
String text = "hello";
//加密
String encrptText = cipherService.encrypt(text.getBytes(), key.getEncoded()).toHex();
//解密
String text2 = new String(cipherService.decrypt(Hex.decode(encrptText), key.getEncoded()).getBytes());
Assert.assertEquals(text, text2);
Shiro的加密接口
- Shiro提供的散列支持
//内部使用MessageDigest
String simpleHash = new SimpleHash("SHA-1", str, salt).toString();
- 经过加密后变成byte数组,必须toString()/toHex()变换一下比较好看,而存入数据库一般是toHex()。
- 为了实现更加完整的加密方案,Shiro提供了HashService,这个也是密码加密最常用的。
DefaultHashService hashService = new DefaultHashService(); //默认实现
//all设置
hashService.setHashAlgorithmName("SHA-512"); //设置算法;默认为SHA-512;被request覆盖
hashService.setPrivateSalt(new SimpleByteSource("123")); //私盐,散列时自动与用户传入
的公盐混合产生一个新盐;默认无
hashService.setGeneratePublicSalt(true); //在用户没有传入公盐时是否自动产生公盐;
被request覆盖
hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator()); //用于生成公盐;
SecureRandomNumberGenerator用于生成一个随机数;默认就这个
hashService.setHashIterations(1); //散列迭代次数;被request覆盖
HashRequest request=new HashRequest.Builder() //all有用配置如下
.setAlgorithmName("MD5").setSource(ByteSource.Util.bytes("hello"))
.setSalt(ByteSource.Util.bytes("123")).setIterations(2).build();
String hex=hashService.computeHash((request)).toHex();
- 代码实例:shiroHelloWorld/test/AllTest
PasswordService、CredentialsMatcher加密和验证密码
- Shiro提供了PasswordService和CredentialsMatcher接口用于加密和验证密码。
- PasswordService默认实现:DefaultPasswordService。
- CredentialsMatcher默认实现:PasswordMatcher、HashedcredentialsMatcher(更强大)。
示例
- 自定义Realm
public class MyRealm4 extends AuthorizingRealm {
public PasswordService passwordService; //等待外部注入
public void setPasswordService(PasswordService passwordService){
this.passwordService=passwordService;
}
/**
* @Author haien
* @Description 被间接父类AuthenticatingRealm调用,获取到AuthenticationInfo后
调用assertCredentialsMatch(token,info),其中token代表表单信息,
info代表数据库用户,它使用了credentialsMatcher
(未指定则使用默认实现类)来验证密码是否匹配,
否则抛出IncorrectCredentialsException;
但不验证用户名是否正确
* @Date 2019/2/22
* @Param [token]
* @return org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
return new SimpleAuthenticationInfo("wu",
passwordService.encryptPassword("123"),getName());
}
/**
* @Author haien
* @Description 不需要授权就直接返回null就好了
* @Date 2019/2/22
* @Param [principalCollection]
* @return org.apache.shiro.authz.AuthorizationInfo
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
- 如果自定义Realm实现的是Realm接口,而不是继承了AuthenticatingRealm及其以下的类,则用户名和密码验证逻辑都必须在此Realm中实现,否则视返回Info类为验证通过,不再进行验证。
- shiro-passwordservice.ini
[main]
passwordService=org.apache.shiro.authc.credential.DefaultPasswordService //有必要可自定义
hashService=org.apache.shiro.crypto.hash.DefaultHashService //定义散列密码使用的HashService
passwordService.hashService=$hashService
hashFormat=org.apache.shiro.crypto.hash.format.Shiro1CryptFormat //对散列出的值格式化,默认使用Shiro1CryptFormat,还有Base64Formath和HexFormat;
对于有salt的密码应自定义ParsableHashFormat实现类,然后把salt格式化到散列值中。
passwordService.hashFormat=$hashFormat
hashFormatFactory=org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory //根据散列值得到散列的密码和salt
passwordService.hashFormatFactory=$hashFormatFactory
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher //定义AuthenticatingRealm
用到的CredentialsMatcher
passwordMatcher.passwordService=$passwordService
myRealm=com.haien.shiroHelloWorld.chapter5.realm.MyRealm
myRealm.passwordService=$passwordService
myRealm.credentialsMatcher=$passwordMatcher
securityManager.realms=$myRealm;
- PasswordTest:测试类
public class PasswordTest extends BaseTest {
@Test
public void testPasswordServiceWithMyRealm(){
login("classpath:config/shiro-passwordservice.ini",
"wu","123");
}
}
使用jdbc的示例
- shiro-jdbc-passwordservice.ini:如果JdbcRealm不指定CredentialsMatcher,则会使用默认实现类SimpleCredentialsMatcher,它不进行加密,只进行明文匹配。
[main]
passwordService=org.apache.shiro.authc.credential.DefaultPasswordService
hashService=org.apache.shiro.crypto.hash.DefaultHashService
passwordService.hashService=$hashService
hashFormat=org.apache.shiro.crypto.hash.format.Shiro1CryptFormat
passwordService.hashFormat=$hashFormat
hashFormatFactory=org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory
passwordService.hashFormatFactory=$hashFormatFactory
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService=$passwordService
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://127.0.0.1:3306/shiro
dataSource.username=root
dataSource.password=123456
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
jdbcRealm.credentialsMatcher=$passwordMatcher
//不用配置PasswordService
securityManager.realms=$jdbcRealm
- PasswordTest:测试类
public class PasswordTest extends BaseTest {
@Test
public void testPasswordserviceWithJdbcRealm(){
login("classpath:config/shiro-jdbc-passwordservice.ini",
"wu","123");
}
}
HashedCredentialsMatcher
- 和之前的PasswordMatcher不同,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且加密算法可以指定。
- 比如使用MD5,“密码+盐(用户名—+随机数”的方式生成散列值。
自定义Realm示例
- MyRealm2:准备用户
/**
* @Author haien
* @Description 用户名+加密后的密码作为盐
* @Date 2019/2/23
**/
public class MyRealm2 extends AuthenticatingRealm { //不需要授权,继承AuthenticatingRealm即可
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//用户库
String username="liu";
String password="123";
//返回后,AuthenticatingRealm只会进行密码比对,不对用户名进行验证,因此用户验证需要在这里实现
if(!username.equals(authenticationToken.getPrincipal())){
throw new UnknownAccountException();
}
//密码加密;则ini配置文件应制定相同的加密方式来对登录用户进行加密后再与realm比对
String algorithmName="md5";
String salt2=new SecureRandomNumberGenerator().nextBytes().toHex(); //随机数
int hashIterations=2;
SimpleHash hash=new SimpleHash(algorithmName,password,
username+salt2,hashIterations);
String encodedPassword=hash.toHex(); //加密后的密码
//封装
SimpleAuthenticationInfo ai=
new SimpleAuthenticationInfo(username,encodedPassword,getName());
//HashedCredentialsMatcher会自动识别这个盐,并拿去给登录用户加密
ai.setCredentialsSalt(ByteSource.Util.bytes(username+salt2)); //盐=用户名+随机数
//返回给AuthenticatingRealm后,它调用HashedCredentialsMatcher的方法,
//将login密码加密,并与此身份凭证中的密码比对
return ai;
}
}
- shiro-hashedCredentialsMatcher.ini:指定CredentialsMatcher实现类、Realm
[main]
;告诉AuthorizingRealm,它获取到的身份凭证是这么来的
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
;加密后的密码是否转为了十六进制,默认是base64
credentialsMatcher.storedCredentialsHexEncoded=true
myRealm=com.haien.shiroHelloWorld.chapter5.realm.MyRealm2
myRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$myRealm
- 测试
public class PasswordTest extends BaseTest {
@Test
public void testHashedCredentialsMatcherWithMyRealm2() {
login("classpath:config/shiro-hashedCredentialsMatcher.ini",
"liu", "123");
//HashedCredentialsMatcher会将密码加密后与Realm提供的用户库进行比对
}
}
JdbcRealm示例
- shiro-jdbc-hashedCredentialsMatcher.ini:Shiro默认不进行Enum类型转换,但saltStyle是枚举类型,因此需要我们自己注册一个Enum转换器对值先进行类型转换再赋给saltStyle。而密码+盐查询语句authenticationQuery原本为:select password, password_salt from users where username = ?,现在我们的盐=username+password_salt,因此要重写sql.
[main]
;指定密码加密方式,需要和用户注册、修改密码时使用的加密方式一致
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://127.0.0.1:3306/shiro
dataSource.username=root
dataSource.password=123456
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
;需要被转换为Enum才能赋给saltStyle属性,因此在测试方法中注册了自定义的Enum转换器
jdbcRealm.saltStyle=COLUMN
;重写sql语句;同时对用户名和密码进行验证
jdbcRealm.authenticationQuery=select password, concat(username,password_salt) from users where username = ?
jdbcRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$jdbcRealm
- 测试方法
public class PasswordTest extends BaseTest {
/**
* @Author haien
* @Description 自定义Enum转换器
* @Date 2019/2/23
**/
private class EnumConverter extends AbstractConverter{
@Override
protected String convertToString(final Object value) throws Throwable {
return ((Enum)value).name();
}
@Override
protected Class getDefaultType() {
return null;
}
@Override
protected Object convertToType(final Class type, final Object value)
throws Throwable {
return Enum.valueOf(type,value.toString());
}
}
@Test
public void testHashedCredentialsMatcherWithJdbcRealm(){
//注册自定义的Enum转换器,否则ini文件默认不进行Enum类型转换
BeanUtilsBean.getInstance().getConvertUtils().register(new EnumConverter(),
JdbcRealm.SaltStyle.class);
login("classpath:config/shiro-jdbc-hashedCredentialsMatcher.ini",
"liu", "123");
}
}
密码重试次数限制
- 目的:防止密码被暴力破解。
- 方法:继承HashedCredentialsMatcher,且使用Ehcache记录重试次数和超时时间。
- RetryLimitHashedCredentialsMatcher:限制输入次数不超出5次
/**
* @Author haien
* @Description 自定义CredentialsMatcher实现类,限制密码重试次数
* 输入正确清除cache中的记录,否则cache中的重试次数+1,如果超出5次那么抛出异常
* @Date 2019/2/24
**/
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();
//获取该用户的缓存对象
Element element=passwordRetryCache.get(username);
//缓存对象不存在说明上一次登录成功或从未登录过
if(element==null){
element=new Element(username,new AtomicInteger(0)); //new一个并置零
passwordRetryCache.put(element);
}
//从缓存对象中获取重试次数
AtomicInteger retryCount=(AtomicInteger)element.getObjectValue();
//第6次起无论输入对错都禁止
if(retryCount.incrementAndGet()>5) //+1
throw new ExcessiveAttemptsException();
boolean matches=super.doCredentialsMatch(token,info);
if(matches)
passwordRetryCache.remove(username);
return matches;
}
}
- 《跟我学Shiro》第五章
- 代码实例:ideaProjects/shiroHelloWorld/chapter5