shiro之編碼/加密

jopen 10年前發布 | 105K 次閱讀 Shiro 安全相關

在涉及到密碼存儲問題上,應該加密/生成密碼摘要存儲,而不是存儲明文密碼。

編碼/解碼

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');

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!