Shiro安全框架学习(三) —— shiro加密处理

md5 加密

 在前面的例子里,用户密码是明文的,这样是有巨大风险的,一旦泄露,就不好了。
所以,通常都会采用非对称加密,什么是非对称呢?就是不可逆的,而 md5 就是这样一个算法。
如代码所示 123 用 md5 加密后,得到字符串: 202CB962AC59075B964B07152D234B70
这个字符串,却无法通过计算,反过来不会得到源密码是 123。
这个加密后的字符串就存在数据库里了,下次用户再登陆,输入密码 123, 同样用md5 加密后,再和这个字符串一比较,就知道密码是否正确了。
如此这样,既能保证用户密码校验的功能,又能保证不暴露密码。


MD5加密代码(法一), java.security.MessageDigest:

package com.ares_bms.util;

import java.security.MessageDigest;

public class Md5Util {

    public static String MD5(String key) {
        char hexDigits[] = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
        };
        try {
            byte[] btInput = key.getBytes();
            // 获得MD5摘要算法的 MessageDigest 对象
            MessageDigest mdInst = MessageDigest.getInstance("MD5");
            // 使用指定的字节更新摘要
            mdInst.update(btInput);
            // 获得密文
            byte[] md = mdInst.digest();
            // 把密文转换成十六进制的字符串形式
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (int i = 0; i < j; i++) {
                byte byte0 = md[i];
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (Exception e) {
            return null;
        }
    }
    
    public static void main(String[] args) {
    	System.out.println(Md5Util.MD5("666"));
	}
}

 MD5加密代码(法二),org.apache.shiro.crypto.hash.Md5Hash:

package com.how2java;

import org.apache.shiro.crypto.hash.Md5Hash;

public class TestEncryption {

    public static void main(String[] args) {
        String password = "123";
        String encodedPassword = new Md5Hash(password).toString();

        System.out.println(encodedPassword);
    }
}

 盐

上面讲了md5加密,但是md5加密又有一些缺陷:

  1. 如果我的密码是 123,你的也是 123,,那么md5的值是一样的,那么通过比较加密后的字符串,我就可以反推过来,原来你的密码也是123。
  2. 与上述相同,虽然 md5 不可逆,但是我可以穷举法呀,我把特别常用的100万或者更多个密码的 md5 值记录下来,比如12345的,abcde的。 相当一部分人用的密码也是这些,那么只要到数据库里一找,也很快就可以知道原密码是多少了。这样看上去也就破解了,至少一部分没有想象中那么安全吧。

为了解决这个问题,引入了盐的概念。 盐是什么意思呢? 比如炒菜,直接使用md5,就是对食材(源密码)进行炒菜,因为食材是一样的,所以炒出来的味道都一样,可是如果加了不同分量的盐,那么即便食材一样,炒出来的味道也就不一样了。

所以,虽然每次 123 md5 之后都是202CB962AC59075B964B07152D234B70,但是 我加上盐,即 123+随机数,那么md5值不就不一样了吗? 这个随机数,就是盐而这个随机数也会在数据库里保存下来,每个不同的用户,随机数也是不一样的。
再就是加密次数,加密一次是202CB962AC59075B964B07152D234B70,我可以加密两次呀,就是另一个数了。 而黑客即便是拿到了加密后的密码,如果不知道到底加密了多少次,也是很难办的。

在代码里就演示了,如何用Shiro自带的工具类,做生成盐,两次md5的做法,如图所示得到一串机密都很高的密文。

package com.how2java;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;

public class TestEncryption {

    public static void main(String[] args) {
        String password = "123";
        String salt = new SecureRandomNumberGenerator().nextBytes().toString(); //Shiro自带的工具类,生成盐
        int times = 2; //加密次数
        String algorithmName = "md5"; //算法名

        String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString();

        System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodedPassword);

    }
}

数据库调整

 有了以上基础,那么就可以开始在原来的教程里加入对加密的支持了。 在开始之前,要修改一下user表,加上盐 字段: salt。
因盐是随机数,得保留下来,如果不知道盐巴是多少,我们也就没法判断密码是否正确了

alter table user add (salt varchar(100) )

 DAO

 增加两个方法 createUser,getUser
createUser 用于注册,并且在注册的时候,将用户提交的密码加密
getUser 用于取出用户信息,其中不仅仅包括加密后的密码,还包括盐

package com.how2java;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;
 
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
 
public class DAO {
    public DAO() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
 
    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root",
                "admin");
    }
     
    public String createUser(String name, String password) {
         
        String sql = "insert into user values(null,?,?,?)";
         
        String salt = new SecureRandomNumberGenerator().nextBytes().toString(); //盐量随机
        String encodedPassword= new SimpleHash("md5",password,salt,2).toString();
         
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
             
            ps.setString(1, name);
            ps.setString(2, encodedPassword);
            ps.setString(3, salt);
            ps.execute();
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
        return null;       
         
    }
 
    public String getPassword(String userName) {
        String sql = "select password from user where name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
             
            ps.setString(1, userName);
             
            ResultSet rs = ps.executeQuery();
 
            if (rs.next())
                return rs.getString("password");
 
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
        return null;
    }
    public User getUser(String userName) {
        User user = null;
        String sql = "select * from user where name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
             
            ps.setString(1, userName);
             
            ResultSet rs = ps.executeQuery();
             
            if (rs.next()) {
                user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setSalt(rs.getString("salt"));
            }
             
        } catch (SQLException e) {
             
            e.printStackTrace();
        }
        return user;
    }
     
    public Set<String> listRoles(String userName) {
         
        Set<String> roles = new HashSet<>();
        String sql = "select r.name from user u "
                + "left join user_role ur on u.id = ur.uid "
                + "left join Role r on r.id = ur.rid "
                + "where u.name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
            ps.setString(1, userName);
            ResultSet rs = ps.executeQuery();
             
            while (rs.next()) {
                roles.add(rs.getString(1));
            }
             
        } catch (SQLException e) {
             
            e.printStackTrace();
        }
        return roles;
    }
    public Set<String> listPermissions(String userName) {
        Set<String> permissions = new HashSet<>();
        String sql =
            "select p.name from user u "+
            "left join user_role ru on u.id = ru.uid "+
            "left join role r on r.id = ru.rid "+
            "left join role_permission rp on r.id = rp.rid "+
            "left join permission p on p.id = rp.pid "+
            "where u.name =?";
         
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
             
            ps.setString(1, userName);
             
            ResultSet rs = ps.executeQuery();
             
            while (rs.next()) {
                permissions.add(rs.getString(1));
            }
             
        } catch (SQLException e) {
             
            e.printStackTrace();
        }
        return permissions;
    }
}

 DatabaseRealm

 修改 DatabaseRealm,把用户通过 UsernamePasswordToken 传进来的密码,以及数据库里取出来的 salt 进行加密,加密之后再与数据库里的密文进行比较,判断用户是否能够通过验证。

package com.how2java;
 
import java.util.Set;
 
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
 
public class DatabaseRealm extends AuthorizingRealm {
 
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
         
        //能进入到这里,表示账号已经通过验证了
        String userName =(String) principalCollection.getPrimaryPrincipal();
        //通过DAO获取角色和权限
        Set<String> permissions = new DAO().listPermissions(userName);
        Set<String> roles = new DAO().listRoles(userName);
         
        //授权对象
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        //把通过DAO获取到的角色和权限放进去
        s.setStringPermissions(permissions);
        s.setRoles(roles);
        return s;
    }
 
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取账号密码
        UsernamePasswordToken t = (UsernamePasswordToken) token;
        String userName= token.getPrincipal().toString();
        String password =new String(t.getPassword());
        //获取数据库中的密码
         
        User user = new DAO().getUser(userName);
        String passwordInDB = user.getPassword();
        String salt = user.getSalt();
        String passwordEncoded = new SimpleHash("md5",password,salt,2).toString();
         
        if(null==user || !passwordEncoded.equals(passwordInDB))
            throw new AuthenticationException();
         
        //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
        SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName());
        return a;
    }
 
}

 shiro.ini

[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true

databaseRealm=com.how2java.DatabaseRealm
securityManager.realms=$databaseRealm

TestShiro

 进行账号密码验证测试。 注意,测试之前要先释放注释,以执行一次注册用户行为,如果不注册,后面肯定是不能验证通过的,因为数据库里没有嘛。释放:new DAO().createUser("tom", "123");

package com.how2java;
 
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.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
 
public class TestShiro {
    public static void main(String[] args) {
        //这里要释放注释,先注册一个用户
//      new DAO().createUser("tom", "123");
         
        User user = new User();
        user.setName("tom");
        user.setPassword("123");
         
        if(login(user))
            System.out.println("登录成功");
        else
            System.out.println("登录失败");
         
    }
     
    private static boolean hasRole(User user, String role) {
        Subject subject = getSubject(user);
        return subject.hasRole(role);
    }
     
    private static boolean isPermitted(User user, String permit) {
        Subject subject = getSubject(user);
        return subject.isPermitted(permit);
    }
 
    private static Subject getSubject(User user) {
        //加载配置文件,并获取工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //获取安全管理者实例
        SecurityManager sm = factory.getInstance();
        //将安全管理者放入全局对象
        SecurityUtils.setSecurityManager(sm);
        //全局对象通过安全管理者生成Subject对象
        Subject subject = SecurityUtils.getSubject();
         
        return subject;
    }
     
    private static boolean login(User user) {
        Subject subject= getSubject(user);
        //如果已经登录过了,退出
        if(subject.isAuthenticated())
            subject.logout();
         
        //封装用户的数据
        UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());
        try {
            //将用户的数据token 最终传递到Realm中进行对比
            subject.login(token);
        } catch (AuthenticationException e) {
            //验证错误
            return false;
        }              
         
        return subject.isAuthenticated();
    }
     
}

扩展:另一个做法的DatabaseRealm

DatabaseRealm 中的做法是自己计算加密后的秘文,再自己比较。

另一个做法是使用Shiro提供的 HashedCredentialsMatcher(ini文件中) 帮我们做。 
在创建 SimpleAuthenticationInfo 的时候,把数据库中取出来的密文以及盐作为参数传递进去。

SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,passwordInDB,ByteSource.Util.bytes(salt),getName());


对比 DatabaseRealm 中的做法,传递的是加密之前的密码,也没有传盐

SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName());

这是第一步,下一步需要 修改 shiro.ini

package com.how2java;
 
import java.util.Set;
 
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
 
public class DatabaseRealm extends AuthorizingRealm {
 
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
         
        //能进入到这里,表示账号已经通过验证了
        String userName =(String) principalCollection.getPrimaryPrincipal();
        //通过DAO获取角色和权限
        Set<String> permissions = new DAO().listPermissions(userName);
        Set<String> roles = new DAO().listRoles(userName);
         
        //授权对象
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        //把通过DAO获取到的角色和权限放进去
        s.setStringPermissions(permissions);
        s.setRoles(roles);
        return s;
    }
 
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println(this.getCredentialsMatcher());
        //获取账号密码
        UsernamePasswordToken t = (UsernamePasswordToken) token;
        String userName= token.getPrincipal().toString();
        //获取数据库中的密码
         
        User user = new DAO().getUser(userName);
        String passwordInDB = user.getPassword();
        String salt = user.getSalt();
         
        //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
        //盐也放进去
        //这样通过shiro.ini里配置的 HashedCredentialsMatcher 进行自动校验
        SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,passwordInDB,ByteSource.Util.bytes(salt),getName());
        return a;
    }
 
}

下一步就是 修改 shiro.ini

另一个做法的DatabaseRealm 中只是提供了密文和盐,那么具体算法怎么指定呢? 那么就
修改shiro.ini:
为DatabaseRealm 指定credentialsMatcher,其中就指定了算法是 md5, 次数为2, storedCredentialsHexEncoded 这个表示计算之后以密文为16进制。

这样Shiro就拿着在subject.log() 时传入的UsernamePasswordToken 中的源密码, 数据库里的密文和盐巴,以及配置文件里指定的算法参数,自己去进行相关匹配了。

[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true //这个表示计算之后以密文为16进制
 
databaseRealm=com.how2java.DatabaseRealm
databaseRealm.credentialsMatcher=$credentialsMatcher //加入这段
securityManager.realms=$databaseRealm

来源于:

http://how2j.cn/k/shiro/shiro-encryption/1726.html

猜你喜欢

转载自blog.csdn.net/weixin_41888813/article/details/81362251