欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

shiro编码和加密代码详解

程序员文章站 2024-02-29 12:31:40
涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。比如之前的600w csdn账号泄露对用户可能造成很大损失,因此应加密/生成不可逆的摘要方式存储。...

涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。比如之前的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编码和加密,希望对大家有所帮助