shiro编码和加密代码详解
涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。比如之前的600w csdn账号泄露对用户可能造成很大损失,因此应加密/生成不可逆的摘要方式存储。
编码/解码
shiro提供了base64和16进制字符串编码/解码的api支持,方便一些编码解码操作。shiro内部的一些数据的存储/表示都使用了base64和16进制字符串。
java代码
string str = "hello"; string base64encoded = base64.encodetostring(str.getbytes()); string str2 = base64.decodetostring(base64encoded); assert.assertequals(str, str2);
通过如上方式可以进行base64编码/解码操作,更多api请参考其javadoc。
java代码
string str = "hello"; string base64encoded = hex.encodetostring(str.getbytes()); string str2 = new string(hex.decode(base64encoded.getbytes())); assert.assertequals(str, str2);
通过如上方式可以进行16进制字符串编码/解码操作,更多api请参考其javadoc。
还有一个可能经常用到的类codecsupport,提供了tobytes(str, "utf-8") / tostring(bytes, "utf-8")用于在byte数组/string之间转换。
散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如md5、sha等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和id(即盐);这样散列的对象是“密码+用户名+id”,这样生成的散列值相对来说更难破解。
java代码
string str = "hello"; string salt = "123"; string md5 = new md5hash(str, salt).tostring();//还可以转换为 tobase64()/tohex()
如上代码通过盐“123”md5散列“hello”。另外散列时还可以指定散列次数,如2次表示:md5(md5(str)):“new md5hash(str, salt, 2).tostring()”。
java代码
string str = "hello"; string salt = "123"; string sha1 = new sha256hash(str, salt).tostring();
使用sha256算法生成相应的散列数据,另外还有如sha1、sha512算法。
shiro还提供了通用的散列支持:
java代码
string str = "hello"; string salt = "123"; //内部使用messagedigest string simplehash = new simplehash("sha-1", str, salt).tostring();
通过调用simplehash时指定散列算法,其内部使用了java的messagedigest实现。
为了方便使用,shiro提供了hashservice,默认提供了defaulthashservice实现。
java代码
defaulthashservice hashservice = new defaulthashservice(); //默认算法sha-512 hashservice.sethashalgorithmname("sha-512"); hashservice.setprivatesalt(new simplebytesource("123")); //私盐,默认无 hashservice.setgeneratepublicsalt(true);//是否生成公盐,默认false hashservice.setrandomnumbergenerator(new securerandomnumbergenerator());//用于生成公盐。默认就这个 hashservice.sethashiterations(1); //生成hash值的迭代次数 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();
1、首先创建一个defaulthashservice,默认使用sha-512算法;
2、可以通过hashalgorithmname属性修改算法;
3、可以通过privatesalt设置一个私盐,其在散列时自动与用户传入的公盐混合产生一个新盐;
4、可以通过generatepublicsalt属性在用户没有传入公盐的情况下是否生成公盐;
5、可以设置randomnumbergenerator用于生成公盐;
6、可以设置hashiterations属性来修改默认加密迭代次数;
7、需要构建一个hashrequest,传入算法、数据、公盐、迭代次数。
securerandomnumbergenerator用于生成一个随机数:
java代码
securerandomnumbergenerator randomnumbergenerator = new securerandomnumbergenerator(); randomnumbergenerator.setseed("123".getbytes()); string hex = randomnumbergenerator.nextbytes().tohex();
加密/解密
shiro还提供对称式加密/解密算法的支持,如aes、blowfish等;当前还没有提供对非对称加密/解密算法支持,未来版本可能提供。
aes算法实现:
java代码
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);
更多算法请参考示例com.github.zhangkaitao.shiro.chapter5.hash.codecandcryptotest。
passwordservice/credentialsmatcher
shiro提供了passwordservice及credentialsmatcher用于提供加密密码及验证密码服务。
java代码
public interface passwordservice { //输入明文密码得到密文密码 string encryptpassword(object plaintextpassword) throws illegalargumentexception; }
java代码
public interface credentialsmatcher { //匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密) boolean docredentialsmatch(authenticationtoken token, authenticationinfo info); }
shiro默认提供了passwordservice实现defaultpasswordservice;credentialsmatcher实现passwordmatcher及hashedcredentialsmatcher(更强大)。
defaultpasswordservice配合passwordmatcher实现简单的密码加密与验证服务
1、定义realm(com.github.zhangkaitao.shiro.chapter5.hash.realm.myrealm)
java代码
public class myrealm extends authorizingrealm { private passwordservice passwordservice; public void setpasswordservice(passwordservice passwordservice) { this.passwordservice = passwordservice; } //省略dogetauthorizationinfo,具体看代码 @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { return new simpleauthenticationinfo( "wu", passwordservice.encryptpassword("123"), getname()); } }
为了方便,直接注入一个passwordservice来加密密码,实际使用时需要在service层使用passwordservice加密密码并存到数据库。
2、ini配置(shiro-passwordservice.ini)
java代码
[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 myrealm=com.github.zhangkaitao.shiro.chapter5.hash.realm.myrealm myrealm.passwordservice=$passwordservice myrealm.credentialsmatcher=$passwordmatcher securitymanager.realms=$myrealm
2.1、passwordservice使用defaultpasswordservice,如果有必要也可以自定义;
2.2、hashservice定义散列密码使用的hashservice,默认使用defaulthashservice(默认sha-256算法);
2.3、hashformat用于对散列出的值进行格式化,默认使用shiro1cryptformat,另外提供了base64format和hexformat,对于有salt的密码请自定义实现parsablehashformat然后把salt格式化到散列值中;
2.4、hashformatfactory用于根据散列值得到散列的密码和salt;因为如果使用如sha算法,那么会生成一个salt,此salt需要保存到散列后的值中以便之后与传入的密码比较时使用;默认使用defaulthashformatfactory;
2.5、passwordmatcher使用passwordmatcher,其是一个credentialsmatcher实现;
2.6、将credentialsmatcher赋值给myrealm,myrealm间接继承了authenticatingrealm,其在调用getauthenticationinfo方法获取到authenticationinfo信息后,会使用credentialsmatcher来验证凭据是否匹配,如果不匹配将抛出incorrectcredentialsexception异常。
另外可以参考配置shiro-jdbc-passwordservice.ini,提供了jdbcrealm的测试用例,测试前请先调用sql/shiro-init-data.sql初始化用户数据。
如上方式的缺点是:salt保存在散列值中;没有实现如密码重试次数限制。
hashedcredentialsmatcher实现密码验证服务
shiro提供了credentialsmatcher的散列实现hashedcredentialsmatcher,和之前的passwordmatcher不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。
1、生成密码散列值
此处我们使用md5算法,“密码+盐(用户名+随机数)”的方式生成散列值:
java代码
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); string encodedpassword = hash.tohex();
如果要写用户模块,需要在新增用户/重置密码时使用如上算法保存密码,将生成的密码及salt2存入数据库(因为我们的散列算法是:md5(md5(密码+username+salt2)))。
2、生成realm(com.github.zhangkaitao.shiro.chapter5.hash.realm.myrealm2)
java代码
protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { string username = "liu"; //用户名及salt1 string password = "202cb962ac59075b964b07152d234b70"; //加密后的密码 string salt2 = "202cb962ac59075b964b07152d234b70"; simpleauthenticationinfo ai = new simpleauthenticationinfo(username, password, getname()); ai.setcredentialssalt(bytesource.util.bytes(username+salt2)); //盐是用户名+随机数 return ai; }
此处就是把步骤1中生成的相应数据组装为simpleauthenticationinfo,通过simpleauthenticationinfo的credentialssalt设置盐,hashedcredentialsmatcher会自动识别这个盐。
如果使用jdbcrealm,需要修改获取用户信息(包括盐)的sql:“select password, password_salt from users where username = ?”,而我们的盐是由username+password_salt组成,所以需要通过如下ini配置(shiro-jdbc-hashedcredentialsmatcher.ini)修改:
java代码
jdbcrealm.saltstyle=column jdbcrealm.authenticationquery=select password, concat(username,password_salt) from users where username = ? jdbcrealm.credentialsmatcher=$credentialsmatcher
1、saltstyle表示使用密码+盐的机制,authenticationquery第一列是密码,第二列是盐;
2、通过authenticationquery指定密码及盐查询sql;
此处还要注意shiro默认使用了apache commons beanutils,默认是不进行enum类型转型的,此时需要自己注册一个enum转换器“beanutilsbean.getinstance().getconvertutils().register(new enumconverter(), jdbcrealm.saltstyle.class);”具体请参考示例“com.github.zhangkaitao.shiro.chapter5.hash.passwordtest”中的代码。
另外可以参考配置shiro-jdbc-passwordservice.ini,提供了jdbcrealm的测试用例,测试前请先调用sql/shiro-init-data.sql初始化用户数据。
3、ini配置(shiro-hashedcredentialsmatcher.ini)
java代码
[main] credentialsmatcher=org.apache.shiro.authc.credential.hashedcredentialsmatcher credentialsmatcher.hashalgorithmname=md5 credentialsmatcher.hashiterations=2 credentialsmatcher.storedcredentialshexencoded=true myrealm=com.github.zhangkaitao.shiro.chapter5.hash.realm.myrealm2 myrealm.credentialsmatcher=$credentialsmatcher securitymanager.realms=$myrealm
1、通过credentialsmatcher.hashalgorithmname=md5指定散列算法为md5,需要和生成密码时的一样;
2、credentialsmatcher.hashiterations=2,散列迭代次数,需要和生成密码时的意义;
3、credentialsmatcher.storedcredentialshexencoded=true表示是否存储散列后的密码为16进制,需要和生成密码时的一样,默认是base64;
此处最需要注意的就是hashedcredentialsmatcher的算法需要和生成密码时的算法一样。另外hashedcredentialsmatcher会自动根据authenticationinfo的类型是否是saltedauthenticationinfo来获取credentialssalt盐。
4、测试用例请参考com.github.zhangkaitao.shiro.chapter5.hash.passwordtest。
密码重试次数限制
如在1个小时内密码最多重试5次,如果尝试次数超过5次就锁定1小时,1小时后可再次重试,如果还是重试失败,可以锁定如1天,以此类推,防止密码被暴力破解。我们通过继承hashedcredentialsmatcher,且使用ehcache记录重试次数和超时时间。
com.github.zhangkaitao.shiro.chapter5.hash.credentials.retrylimithashedcredentialsmatcher:
java代码
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次那么抛出异常表示超出重试次数了。
总结
以上所述是小编给大家介绍的shiro编码和加密,希望对大家有所帮助
上一篇: Java设计模式之动态代理
下一篇: 教你构建第一个Java Applet程序