shiro之编码/加密

jopen 10年前

在涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。

编码/解码

shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。shiro内部的一些数据的存储/表示都使用base64和16进制字符串。如例:

package org.shiro.t1;    import junit.framework.Assert;  import org.apache.shiro.codec.Base64;  import org.apache.shiro.codec.Hex;  import org.junit.Test;    public class Shiro_1 {      @Test      public void test(){          String str1 = "hello";          //base64进行编码          String base64Encoded = Base64.encodeToString(str1.getBytes());          //base64进行解码          String str2 = Base64.decodeToString(base64Encoded);          //16进制编码          String hexEncoded = Hex.encodeToString(str1.getBytes());          //16进制解码          String str3 = new String(Hex.decode(hexEncoded.getBytes()));          Assert.assertEquals(str1,str2);          Assert.assertEquals(str1, str3);      }  }

散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如:MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码"admin",产生的散列值是 "21232f297a57a5a743894a0e4a801fc3",可以到一些md5解密网站很容易的通过散列值得到密码 "admin",即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是 "密码+用户名+ID",这样生成的散列值相对来说更难破解。如例:

package org.shiro.t1;    import org.apache.shiro.crypto.hash.Md5Hash;  import org.apache.shiro.crypto.hash.Sha256Hash;  import org.junit.Test;    public class Shiro_2 {      @Test      public void test(){          String str1 = "hello";          String salt = "123";          String md5Str1 = new Md5Hash(str1, salt).toString();          System.out.println("MD5值1:" + md5Str1);          //还可以把MD5加密后的值再转成Base64或16进制的编码形式          String md5Str2 = new Md5Hash(str1, salt).toBase64();          String md5Str3 = new Md5Hash(str1, salt).toHex();          System.out.println("MD5值2:" + md5Str2);          System.out.println("MD5值3:" + md5Str3);          //还可以指定加密次数,这里散列加密3次          String md5Str4 = new Md5Hash(str1, salt, 3).toString();          System.out.println("MD5值4:" + md5Str4);                    System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");                    //使用SHA256加密算法          String shaStr1 = new Sha256Hash(str1,salt).toString();          String shaStr2 = new Sha256Hash(str1, salt).toBase64();          String shaStr3 = new Sha256Hash(str1, salt).toHex();          String shaStr4 = new Sha256Hash(str1, salt,3).toString();          System.out.println("SHA256值1:" + shaStr1);          System.out.println("SHA256值2:" + shaStr2);          System.out.println("SHA256值3:" + shaStr3);          System.out.println("SHA256值4:" + shaStr4);      }  }

如上代码,通过盐 "123",MD5与SHA256散列 "hello" 。另外散列时还可以指定散列次数。另外还有如SHA1、SHA512算法。

另外,shiro还提供了通用的散列支持,如下例:

package org.shiro.t1;    import org.apache.shiro.crypto.hash.SimpleHash;  import org.junit.Test;    public class Shiro_3 {      @Test      public void test(){          String str = "hello";          String salt = "123";          String simpleHash = new SimpleHash("SHA-1", str, salt).toString();          System.out.println(simpleHash);      }  }

通过调用SimpleHash时指定散列算法,其内部使用了java的MessageDigest实现。

为了方便使用,shiro提供了HashService,默认提供了DefaultHashService实现,见下例:

package org.shiro.t1;    import org.apache.shiro.crypto.SecureRandomNumberGenerator;  import org.apache.shiro.crypto.hash.DefaultHashService;  import org.apache.shiro.crypto.hash.HashRequest;  import org.apache.shiro.util.ByteSource;  import org.apache.shiro.util.SimpleByteSource;  import org.junit.Test;    public class Shiro_4 {      @Test      public void test(){          //默认算法SHA-512          DefaultHashService hashService = new DefaultHashService();          hashService.setHashAlgorithmName("SHA-512");          //私盐,默认无          hashService.setPrivateSalt(new SimpleByteSource("123"));          //是否生成公盐,默认false          hashService.setGeneratePublicSalt(true);          //用于生成公盐,默认就这个          hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());          //生成Hash值的迭代次数          hashService.setHashIterations(1);          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();          System.out.println(hex);      }  }

  1. 首先创建一个DefaultHashService,默认使用SHA-512算法。

2.    可以通过hashAlgorithmName属性修改算法

3.    可以通过privateSalt设置一个私盐,其在散列时自动与用户传入的公盐混合产生一个新盐

4.    可以通过generatePublicSalt属性在用户没有传入公盐的情况下是否生成公盐

5.    可以设置randomNumberGenerator用于生成公盐

6.    可以设置hashIterations属性来修改默认加密迭代次数

7.    需要构建一个HashRequest,传入算法、数据、公盐、迭代次数。

加密/解密

shiro还提供对称式加密/解密算法的支持,如:AES、Blowfish等;AES算法实现的示例:

package org.shiro.t1;    import java.security.Key;    import junit.framework.Assert;    import org.apache.shiro.codec.Hex;  import org.apache.shiro.crypto.AesCipherService;  import org.junit.Test;    public class Shiro_5 {      @Test      public void test(){          AesCipherService aesCipherService = new AesCipherService();          //设置key长度          aesCipherService.setKeySize(128);          //生成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);      }  }

PasswordService/CredentialsMatcher

shiro提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务。

public interface PasswordService {      //输入明文密码得到密文密码      String encryptPassword(Object plaintextPassword) throws IllegalArgumentException;  }

public interface CredentialsMatcher {      //匹配用户输入的token的凭证(未加密)与系统提供凭证(已加密)      boolean doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info);  }

shiro默认提供了PasswordService的实现DefaultPasswordService;CredentialsMatcher的实现PasswordMatcher及HashedCredentialsMatcher(更强大)。

DefaultPasswordService配合PasswordMatcher实现简单的密码加密与验证服务,如例:

public class MyRealm extends AuthorizingRealm {      private PasswordService passwordService;      public void setPasswordService(PasswordService passwordService){          this.passwordService = passwordService;      }      //省略了doGetAthorizationInfo方法      @Override      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)          throws AuthenticationException{          return new SimpleAuthenticationInfo("wu",              passwordService.encryptPassword("123"),getName());      }  }

为了方便,直接注入一个passwordService来加密密码,实际使用时需要在Service层使用passwordService加密密码并存到数据库。

下面是它的ini配置文件:

[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=shiro.t1.MyRealm #自定义  myRealm.passwordService=$passwordService  myRealm.credentialsMatcher=$passwordMatcher  securityManager.realms=$myRealm

1.    passwordService使用DefaultPasswordService,如果有必要也可以自定义;

2.    hashService定义散列密码使用HashService,默认使用DefaultHashService(默认SHA-256算法)

3.    hashFormat用于对散列出的值进行格式化,默认使用Shiro1CryptFormat,另外提供了Base64Format和HexFormat,对于有salt的密码请自定义实现ParsableHashFormat然后把salt格式化到散列值中;

4.    hashFormatFactory用于根据散列值得到散列的密码和salt;因为如果使用如SHA算法,那么会生成一个salt,此salt需要保存到散列后的值中以便之后与传入的密码比较时使用;默认使用DefaultHashFormatFactory;

5.    passwordMatcher使用PasswordMatcher,它是一个CredentialsMatcher实现;

6.    将credentialsMatcher赋值给myRealm,myRealm间接继承了AuthenticatingRealm,其在调用getAuthenticationInfo方法获取到AuthenticationInfo信息后,会使用credentialsMatcher来验证凭据是否匹配,如果不匹配将抛出IncorrectCredentialsException异常。

我们再来看一个完整的实现(JDBC)

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>      <groupId>apl</groupId>      <artifactId>shiro-test</artifactId>      <version>0.0.1-SNAPSHOT</version>        <dependencies>          <dependency>              <groupId>junit</groupId>              <artifactId>junit</artifactId>              <version>4.8.2</version>              <scope>test</scope>          </dependency>          <dependency>              <groupId>commons-logging</groupId>              <artifactId>commons-logging</artifactId>              <version>1.1.3</version>          </dependency>          <dependency>              <groupId>org.apache.shiro</groupId>              <artifactId>shiro-core</artifactId>              <version>1.2.3</version>          </dependency>          <dependency>              <groupId>mysql</groupId>              <artifactId>mysql-connector-java</artifactId>              <version>5.1.25</version>          </dependency>          <dependency>              <groupId>com.alibaba</groupId>              <artifactId>druid</artifactId>              <version>0.2.23</version>          </dependency>      </dependencies>  </project>

shiro-jdbc-passwordservice.ini

[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://localhost:3306/shiro  dataSource.username=root  dataSource.password=000000    jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm  jdbcRealm.dataSource=$dataSource  jdbcRealm.permissionsLookupEnabled=true    jdbcRealm.credentialsMatcher=$passwordMatcher  securityManager.realms=$jdbcRealm

PasswordTest.java

package org.shiro.t2;    import junit.framework.Assert;    import org.apache.shiro.SecurityUtils;  import org.apache.shiro.authc.AuthenticationException;  import org.apache.shiro.authc.UsernamePasswordToken;  import org.apache.shiro.config.IniSecurityManagerFactory;  import org.apache.shiro.subject.Subject;  import org.apache.shiro.util.Factory;  import org.junit.Test;    public class PasswordTest {        @Test      public void testPasswordServiceWithJdbcRealm(){          //获取SecurityManager工厂,此处使用ini配置文件初始化SecurityManager          Factory<org.apache.shiro.mgt.SecurityManager> factory =                   new IniSecurityManagerFactory("classpath:shiro-jdbc-passwordservice.ini");          //得到SecurityManager实例并绑定给SecurityUtils          org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();          SecurityUtils.setSecurityManager(securityManager);          //得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)          Subject subject = SecurityUtils.getSubject();          UsernamePasswordToken token = new UsernamePasswordToken("wu","123");          try {              subject.login(token);          } catch (AuthenticationException e) {              System.err.println(e.getMessage());          }          boolean b = subject.isAuthenticated();          Assert.assertTrue(b);          //退出          subject.logout();      }  }

mysql表结构:

drop database if exists shiro;  create database shiro;  use shiro;    create table users (    id bigint auto_increment,    username varchar(100),    password varchar(100),    password_salt varchar(100),    constraint pk_users primary key(id)  ) charset=utf8 ENGINE=InnoDB;  create unique index idx_users_username on users(username);    create table user_roles(    id bigint auto_increment,    username varchar(100),    role_name varchar(100),    constraint pk_user_roles primary key(id)  ) charset=utf8 ENGINE=InnoDB;  create unique index idx_user_roles on user_roles(username, role_name);    create table roles_permissions(    id bigint auto_increment,    role_name varchar(100),    permission varchar(100),    constraint pk_roles_permissions primary key(id)  ) charset=utf8 ENGINE=InnoDB;  create unique index idx_roles_permissions on roles_permissions(role_name, permission);    insert into shiro.users(username, password, password_salt)       values('wu', '$shiro1$SHA-512$1$$PJkJr+wlNU1VHa4hWQuybjjVPyF              zuNPcPu5MBH56scHri4UQPjvnumE7MbtcnDYhTcnxSkL9ei/bhIVrylxEwg==', null);  insert into shiro.users(username, password, password_salt)       values('liu', 'a9a114054aa6758184314fbb959fbda4', '24520ee264eab73ec09451d0e9ea6aac');