Spring Boot2整合Shiro(2):加密

因为新项目没有继续用Shiro框架,再加上人比较懒,所以就没有再写新的关于 Shiro 的文章。但是今天发现已经有几个朋友在 GitHub 上给示例项目加星,也有留言咨询的。我很高兴这篇文章能够帮到大家,也再次让我感受到把在工作中学到的东西写出来,和大家分享是这么一件令人高兴的事,用知乎上的话说这是一件长半衰期的事。我会继续写下去的

前言

上一篇文章,我们用 Spring Boot2 框架搭建了一个 web 项目,并且使用 Shiro 作为安全管理框架实现了用户的身份认证,也就是登录。这篇文章首先简要介绍了密码储存的演进史,然后结合代码介绍了在 Shiro 中怎么使用 MD5、MD5 加盐、 Bcrypt 等三种逐渐进步的方法加密密码。

密码存储演进史

在上一篇文章中我们保存在系统的用户密码还是明文,这显然是很不合理和不专业的做法。作为一名工程师 专业自然是必须的。

常用的做法是使用加密算法对用户密码加密然后存储数据库,当用户登录时,对用户密码使用相同的算法进行加密,然后与数据库中的密文进行比对。作为一个合格专业的系统有两点需要保证:

  1. 用户账号不易被破解
  2. 系统不能保存用户密码原文

关于密码存储演进史这篇文章讲的很好,朋友们可以看一下。从文章中我们知道密码储存大概分为几个阶段:

  • 明文储存
  • 单向 hash(md5、sha256)
  • 单向 has h加盐
  • 慢加密算法(Bcrypt、Scrypt)

加密

注册用户

因为要演示使用 Shiro 对密码进行加密,上一节中的那种保存密码的方式非常不方便。在这一节我们添加一个用户注册的接口,使用用户对象保存用户名和用户密码并且用一个集合保存系统已注册的用户。

新建用户类,并重写toString方法。


package top.zhaodongxx.domain;

/**
 * @author zhaodong zhaodongxx@outlook.com
 * @version v1.0
 * @since 2018/7/19 10:40
 */
public class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

ShiroService 类中添加新增用户的方法 addUser ,我们使用 SimpleHash 类使用 MD5 加密算法对密码进行加密,然后在提供一个通过用户名查询用户的方法,用于后面的登录验证。


package top.zhaodongxx.service;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.stereotype.Service;
import top.zhaodongxx.domain.User;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhaodong zhaodongxx@outlook.com
 * @version v1.0
 * @since 2018/3/30 22:59
 */
@Service
public class ShiroService {

    public static List<User> userList = new ArrayList<User>();

    public String getPasswordByUsername(String username) {
        switch (username) {
            case "liming":
                return "123";
            case "hanli":
                return "456";
            default:
                return null;
        }
    }

    //根据用户名查询用户
    public User getUserByUsername(String username) {
        System.out.println("登陆的用户 " + username);
        for (User user : userList) {
            if (username.equals(user.getUsername())) {
                return user;
            }
        }
        return null;
    }

    //添加用户
    public User addUser(String username, String password) {
        //通过MD5的方式加密密码
        String newpwd = new SimpleHash("MD5", password).toHex();

        User user = new User();
        user.setUsername(username);
        user.setPassword(newpwd);

        userList.add(user);

        System.out.println(user);
        return user;
    }
}

新建注册控制器类 RegisterController ,并提供注册接口 /register


package top.zhaodongxx.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import top.zhaodongxx.domain.User;
import top.zhaodongxx.result.Result;
import top.zhaodongxx.result.ResultGenerator;
import top.zhaodongxx.service.ShiroService;

import javax.annotation.Resource;

/**
 * @author zhaodong zhaodongxx@outlook.com
 * @version v1.0
 * @since 2018/7/19 10:53
 */
@RestController
public class RegisterController {

    @Resource
    private ShiroService shiroService;

    @PostMapping("/register")
    public Result register(String username, String password) {

        User user = shiroService.addUser(username, password);
        return ResultGenerator.genSuccessResult(user).setMessage("注册成功");
    }
}

如下体所示到这一步,我们就已经新建了一个用户注册的接口。

图片

为Shiro配置加密算法

配置hash算法加密主要分为三步:

  • 创建凭证匹配器 HashedCredentialsMatcher
  • 为我们自定义的 MyShiroRealm Bean 添加凭证匹配器 HashedCredentialsMatcher
  • 修改 MyShiroRealmdoGetAuthenticationInfo 方法的获取用户的方法

在 Shiro 配置文件 ShiroConfig 中创建凭证匹配器 HashedCredentialsMatcher


/**
 * 凭证匹配器
 *
 * @return
 */
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    /**
        * hash算法:这里使用MD5算法;
        */
    hashedCredentialsMatcher.setHashAlgorithmName("MD5");
    /**
        * 散列的次数,比如散列两次,相当于 md5(md5(""));
        */
    hashedCredentialsMatcher.setHashIterations(1);

    return hashedCredentialsMatcher;
}

在 Shiro 配置文件 ShiroConfig 中为我们自定义的 MyShiroRealm Bean 添加凭证匹配器 HashedCredentialsMatcher


/**
 * 自定义的Realm
 */
@Bean(name = "myShiroRealm")
public MyShiroRealm myShiroRealm(){
    MyShiroRealm myShiroRealm = new MyShiroRealm();
    myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
    return myShiroRealm;
}

修改 MyShiroRealmdoGetAuthenticationInfo 方法


@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //获取用户账号
    String username = token.getPrincipal().toString();

    User user = shiroService.getUserByUsername(username);
    if (!ObjectUtils.isEmpty(user)) {

        String realmName = getName();
        return new SimpleAuthenticationInfo(username, user.getPassword(), realmName);
    }
    return null;
}

到目前为止,我们已经成功的为 Shiro 配置了 MD5 这种加密方式。
下面我们来测试一下,先重启项目,注册新的账号,然后等新账号登录。如下图所示,登录成功。

图片
图片

单向hash加盐

单向hash算法,保证了即使是系统管理员也不能轻易的通过密文反推出密码原文。但是单向 hash 还是有个致命的缺点:相同的密码,hash值一样

改进的措施是引入一个 随机的因子 掺杂进明文中进行 hash 计算,这样的随机因子通常被称之为盐 (salt)。salt 一般是用户相关的,且要保证在一个系统内每一个用户的盐都不相同,这样就可以保证即使密码相同,hash 值也一定不相同。一般的做法是用用户表中的主键作为盐。因为在我们这个工程中没有数据库,所以我们把用户名作为盐来示范一下。

为 Shiro 的单项 hash 算法加上盐需要两步:

  • 修改注册用户的加密算法
  • 修改 MyShiroRealm 类中 doGetAuthenticationInfo 方法的返回值 SimpleAuthenticationInfo 的新建方法参数:盐

修改注册用户的加密算法

String newpwd = new SimpleHash("MD5", password, ByteSource.Util.bytes(username)).toHex();

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户账号
        String username = token.getPrincipal().toString();

        User user = shiroService.getUserByUsername(username);
        if (!ObjectUtils.isEmpty(user)) {

            /**
             * 四个参数
             * principal:认证的实体信息,可以是username,也可以是数据库表对应的用户的实体对象
             * credentials:数据库中的密码(经过加密的密码)
             * credentialsSalt:盐值(使用用户名)
             * realmName:当前realm对象的name,调用父类的getName()方法即可
             */

            String realmName = getName();
            String credentials = user.getPassword();
            ByteSource credentialsSalt = ByteSource.Util.bytes(username);

            return new SimpleAuthenticationInfo(username, credentials, credentialsSalt, realmName);
        }
        return null;
    }

Bcrypt

Shiro 框架没有内置 BCrypt 。需要我们引入新的库 jBCrypt:

<dependency>
    <groupId>de.svenkubiak</groupId>
    <artifactId>jBCrypt</artifactId>
    <version>0.4.1</version>
</dependency>

在注册用户时修改加密方式,在 BCrypt 中我们不需要为每个用户分配不同的盐,只要使用 BCrypt.gensalt() 就可以生成盐。

public String encodeByBCrypt(String password) {
    return BCrypt.hashpw(password, BCrypt.gensalt());
}

在 Shiro 配置文件 ShiroConfig 中为我们自定义的 MyShiroRealm Bean 添加凭证匹配器 HashedCredentialsMatcher


/**
 * 自定义的Realm
 */
@Bean(name = "myShiroRealm")
public MyShiroRealm myShiroRealm() {
    MyShiroRealm myShiroRealm = new MyShiroRealm();
    //配置单项hash
    //myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

    //配置 BCrypt
    myShiroRealm.setCredentialsMatcher(new CredentialsMatcher() {
        @Override
        public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            //要验证的明文密码
            String plaintext = new String(userToken.getPassword());
            //数据库中的加密后的密文
            String hashed = info.getCredentials().toString();

            return BCrypt.checkpw(plaintext, hashed);
        }
    });
    return myShiroRealm;
}

到此为止,我们使用 BCrypt 算法加密了密码。如下图所示:

图片
图片
图片

可以发现,相同密码加密两次后的密文是不一样的。

至此,为 Shiro 使用 BCrypt 算法加密了密码。

项目下载地址

参考资料