AES算法加密解密工具类util之改进之动态AES密钥加密

Posted 修罗debug

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AES算法加密解密工具类util之改进之动态AES密钥加密相关的知识,希望对你有一定的参考价值。

AES算法加密解密工具类util之改进之动态AES密钥加密

    对于AES算法,我想很多博友都知晓是干嘛用的,本博文就不详细介绍了。作为一种常用的加密算法,AES加密解密我觉得要点在于其key(密钥),一般项目应用中,aesKey是固定的。本文将基于传统的aes加密解密的写法,介绍一种“基于redis缓存动态aes密钥”的方法。

    顾名思义,动态aes密钥,其实就是使得key动态隔一段在变化,而且又不影响原有存在的密码,即在动态自动更换密钥时,需要使用原有的key进行解密再使用新生成的aesKey进行加密,并将新的aesKey进行存储。

    以上即为缓存动态密钥进行加密解密的思路。下面首先介绍一下固定aesKey的写法:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.UUID;

/**
 * Created by debug on 2017/10/15.
 */
public class TestUtil 

    private static final Logger log= LoggerFactory.getLogger(TestUtil.class);

    public static void main(String[] args)
        /*EnumSet<SourceEnum> set=EnumSet.allOf(SourceEnum.class);
        for(SourceEnum e:set)
            log.debug("enum值: , ",e.toString(),e.getCode());
        */

        String aesKey= "36c82834-3fe4-4305-b6e0-39d52e5113d4";
        String password="123456";
        String resPass=new String(encryptAES(password,aesKey));
        log.debug("aesKey= 加密的结果:  ",aesKey,resPass);

        String srcPass=new String(decryptAES(encryptAES(password,aesKey),aesKey));
        log.debug("解密的结果:  ",srcPass);

    

    /**
     * AES加密
     *
     * @param content   需要加密的内容
     * @param key  加密密钥
     * @return
     */
    public static byte[] encryptAES(String content, String key) 
        try 
            KeyGenerator kgen = KeyGenerator.getInstance("AES");
            kgen.init(128, new SecureRandom(key.getBytes()));
            SecretKey secretKey = kgen.generateKey();
            byte[] enCodeFormat = secretKey.getEncoded();
            SecretKeySpec keySpec = new SecretKeySpec(enCodeFormat, "AES");
            Cipher cipher = Cipher.getInstance("AES");// 创建密码器
            byte[] byteContent = content.getBytes("utf-8");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);// 初始化
            byte[] result = cipher.doFinal(byteContent);
            return result; // 加密
        catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    /**
     * AES解密
     * @param content  待解密内容
     * @param key 解密密钥
     * @return
     */
    public static byte[] decryptAES(byte[] content, String key) 
        try 
            KeyGenerator kgen = KeyGenerator.getInstance("AES");
            kgen.init(128, new SecureRandom(key.getBytes()));
            SecretKey secretKey = kgen.generateKey();
            byte[] enCodeFormat = secretKey.getEncoded();
            SecretKeySpec keySpec = new SecretKeySpec(enCodeFormat, "AES");
            Cipher cipher = Cipher.getInstance("AES");// 创建密码器
            cipher.init(Cipher.DECRYPT_MODE, keySpec);// 初始化
            byte[] result = cipher.doFinal(content);
            return result; // 加密
        catch (Exception e) 
            e.printStackTrace();
        
        return null;
    


    如果,你只是需要一种比较安全,简单的aes加密解密写法,你可以参考上面的代码即可(不过,还是希望使用前 自己改造一番,比如不要硬编码之类的)

    下面就结合“用户注册与登录”的应用场景讲解“缓存动态密钥加密解密”:

   (1)首先创建 注册的user表以及动态变化的aesKey表

create table tb_user
(
	id int auto_increment
		primary key,
	username varchar(255) null comment '用户名',
	password varchar(255) null comment '密码',
	create_time datetime null comment '创建时间',
	constraint idx_username
		unique (username)
)
;

create table tb_encrypt_key
(
	id int auto_increment comment '主键'
		primary key,
	alg_key varchar(255) not null comment '加密key',
	create_time datetime null comment '创建时间'
)
;

   (2)逆向生成mapper与model我就不多说了(不懂的话,可以加入后面的群或者看我以往的博客: mybatis逆向 生成model与mapper工具的博文)

   (3)开发UserService与UserController组件

package com.debug.springboot.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * v1、对key进行Base64编码 作为key - 只是增加了原始key的复杂度
 * v2、对明文密码已经处理完成的二进制串进行过Base64编码解码-可以解码
 * Created by debug on 2017/10/21.
 */
@Service
public class UserService 

    //AES 算法
    private static final String Encrypt_Alg="AES";
    //加密串时选用的字符编码
    private static final String Char_Unicode="UTF-8";
    //keyGen位数
    private static final Integer Key_Size=128;
    private static final Logger log= LoggerFactory.getLogger(UserService.class);

    /**
     * 加密密码
     * @param passwordStr
     * @return
     */
    public String encryptPassword(String passwordStr,String key)
        byte[] encryptBytes=encrypt(passwordStr,key);
        String encryptStr=parseByte2HexStr(encryptBytes);
        return encryptStr;
    

    /**
     * 解密密码
     * @param passwordHex
     * @return
     */
    public String decryptPassword(String passwordHex,String key)
        byte[] decryptBytes=decrypt(parseHexStr2Byte(passwordHex),key);
        String decryptStr=new String(decryptBytes);
        return decryptStr;
    


    /**
     * 加密
     * @param content
     * @return
     */
    private byte[] encrypt(String content,String aesKey) 
        try
            KeyGenerator kgen = KeyGenerator.getInstance(Encrypt_Alg);
            kgen.init(Key_Size,new SecureRandom(Base64.getEncoder().encode(aesKey.getBytes())));  //v3
            kgen.init(Key_Size,new SecureRandom(aesKey.getBytes()));
            SecretKey secretKey = kgen.generateKey();
            byte[] enCodeFormat = secretKey.getEncoded();
            SecretKeySpec key = new SecretKeySpec(enCodeFormat,Encrypt_Alg);
            /**创建密码器**/
            Cipher cipher = Cipher.getInstance(Encrypt_Alg);
            byte[] byteContent = content.getBytes(Char_Unicode);
            /**初始化密码器**/
            cipher.init(Cipher.ENCRYPT_MODE, key);

            byte[] result = cipher.doFinal(byteContent);  //v1

            //byte[] result = Base64.getEncoder().encode(cipher.doFinal(byteContent));  //v2

            return result;
        catch(Exception e) 
            log.error("加密发生异常:  ",content,e.fillInStackTrace());
        
        return null;
    

    /**
     * 解密
     * @param content
     * @return
     */
    private byte[] decrypt(byte[] content,String aesKey) 
        try
            KeyGenerator kgen = KeyGenerator.getInstance(Encrypt_Alg);
            kgen.init(Key_Size,new SecureRandom(Base64.getEncoder().encode(aesKey.getBytes())));  //--解密不了 v3

            kgen.init(Key_Size,new SecureRandom(aesKey.getBytes()));
            SecretKey secretKey = kgen.generateKey();
            byte[] enCodeFormat = secretKey.getEncoded();
            SecretKeySpec key = new SecretKeySpec(enCodeFormat,Encrypt_Alg);
            /**创建密码器**/
            Cipher cipher = Cipher.getInstance(Encrypt_Alg);
            /**初始化密码器**/
            cipher.init(Cipher.DECRYPT_MODE, key);

            byte[] result = cipher.doFinal(content); //v1

            //byte[] result=cipher.doFinal(Base64.getDecoder().decode(content));  //v2

            return result;
        catch(Exception e) 
            log.debug("解密过程发生异常:  ",content,e.fillInStackTrace());
        
        return null;
    

    /**
     * 将二进制转换成十六进制
     * @param buf
     * @return
     */
    private String parseByte2HexStr(byte buf[]) 
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < buf.length; i++) 
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if(hex.length() == 1) 
                hex = '0'+ hex;
            
            sb.append(hex.toUpperCase());
        
        return sb.toString();
    

    /**
     * 将十六进制转换为二进制
     * @param hexStr
     * @return
     */
    private byte[] parseHexStr2Byte(String hexStr) 
        if(hexStr.length() < 1) 
            return null ;
        else
            byte[] result =new byte[hexStr.length() / 2];
            for(int i = 0; i < hexStr.length() / 2; i++) 
                int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
                int low = Integer.parseInt(hexStr.substring(i * 2 + 1,i * 2 + 2),16);
                result[i] = (byte) (high * 16 + low);
            
            return result;
        
    




    上面我还尝试了两种写法,就是加上Base64编码解码增加复杂度而已。

package com.debug.springboot.controller;

import com.debug.springboot.mapper.TbEncryptKeyMapper;
import com.debug.springboot.mapper.TbUserMapper;
import com.debug.springboot.model.TbEncryptKey;
import com.debug.springboot.model.TbUser;
import com.debug.springboot.response.BaseResponse;
import com.debug.springboot.response.Status;
//import com.debug.springboot.utils.AESUtil;
import com.debug.springboot.service.UserService;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Created by debug on 2017/10/20.
 */
@RestController
@EnableScheduling
public class UserController 

    private static final Logger log= LoggerFactory.getLogger(UserController.class);

    private static final String prex="user";

    @Autowired
    private UserService userService;

    @Autowired
    private TbEncryptKeyMapper keyMapper;

    @Autowired
    private TbEncryptKeyMapper encryptKeyMapper;

    @Autowired
    private TbUserMapper userMapper;

    @Autowired
    private Environment env;

    @Autowired
    private StringRedisTemplate redisTemplate;

    //AES加密需要的key
    //@Value("$encrypt.aes.key")
    private String aesKey;

    @PostConstruct
    public void init() throws Exception
        aesKey=keyMapper.selectNewestKey();
        log.debug("当前的key:  ",aesKey);
    

    //此即为动态更换aes密钥并进行缓存的核心代码
    @Scheduled(cron = "0 0/45 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void scheduledUpdateKey() throws Exception
        if (!redisTemplate.hasKey(env.getProperty("redis.key")))
            String newKey= UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(env.getProperty("redis.key"),newKey,env.getProperty("redis.key.timeout",Long.class), TimeUnit.MINUTES);
            TbEncryptKey keyEntity=new TbEncryptKey();
            keyEntity.setCreateTime(new Date());
            keyEntity.setAlgKey(newKey);

            List<TbUser> userAll=userMapper.selectAll();
            for(TbUser u:userAll)
                log.debug("当前用户: key: 密码:  ",u.getUsername(),aesKey,u.getPassword());
                String uPass=userService.decryptPassword(u.getPassword(),aesKey);
                log.debug("解密:  ",uPass);
                u.setPassword(userService.encryptPassword(uPass,newKey));
                userMapper.updateByPrimaryKeySelective(u);
            

            log.debug("oldKey=  已过期, newKey= ",aesKey,newKey);
            aesKey=newKey;
            encryptKeyMapper.insertSelective(keyEntity);

            log.debug("现在真正的key:  ",aesKey);
        
    


    /**
     * 用户注册
     * @param userName
     * @param password
     * @return
     * @throws Exception
     */
    @RequestMapping(value = prex+"/register",method = RequestMethod.POST)
    public BaseResponse register(@RequestParam("userName") String userName,@RequestParam("password") String password) throws Exception
        if (Strings.isNullOrEmpty(userName) || Strings.isNullOrEmpty(password))
            return new BaseResponse(Status.Invalid_Params);
        
        if (userMapper.selectByUserName(userName)!=null)
            return new BaseResponse(Status.User_Name_Has_Exist);
        
        BaseResponse response=new BaseResponse(Status.Success);
        try 

            TbUser user=new TbUser();
            user.setUsername(userName);
            //user.setPassword(AESUtil.encryptPassword(password));
            user.setPassword(userService.encryptPassword(password,this.aesKey));
            user.setCreateTime(new Date());
            userMapper.insertSelective(user);

            Map<String,Object> dataMap= Maps.newHashMap();
            //dataMap.put("pass",AESUtil.decryptPassword(user.getPassword()));
            dataMap.put("pass",userService.decryptPassword(user.getPassword(),this.aesKey));

            response.setData(dataMap);
        catch (Exception e)
            log.error("用户注册发生异常:, ",userName,password,e.fillInStackTrace());
            return new BaseResponse(Status.Fail);
        
        return response;
    

    /**
     * 用户登录
     * @param userName
     * @param password
     * @return
     * @throws Exception
     */
    @RequestMapping(value = prex+"/login",method = RequestMethod.POST)
    public BaseResponse login(@RequestParam("userName") String userName,@RequestParam("password") String password) throws Exception
        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password))
            return new BaseResponse(Status.Invalid_Params);
        
        TbUser user=userMapper.selectByUserName(userName);
        if (user==null)
            return new BaseResponse(Status.User_Not_Exist);
        
        BaseResponse response=new BaseResponse(Status.Success);
        try 
            String passwordHexStr=user.getPassword();
            //String passwordSrcHexStr=AESUtil.encryptPassword(password);
            String passwordSrcHexStr=userService.encryptPassword(password,this.aesKey);
            log.debug("user的16进制密码串:   待比较的16进制密码串: ",passwordHexStr,passwordSrcHexStr);
            if (passwordSrcHexStr.equals(passwordHexStr))
                Map<String,Object> dataMap=Maps.newHashMap();
                dataMap.put("userName",user.getUsername());
                //dataMap.put("password",AESUtil.decryptPassword(passwordHexStr));
                dataMap.put("password",userService.decryptPassword(passwordHexStr,this.aesKey));

                response.setData(dataMap);
            else
                return new BaseResponse(Status.User_Password_Not_Match);
            
        catch (Exception e)
            log.error("用户登录发生异常:, ",userName,password,e.fillInStackTrace());
            return new BaseResponse(Status.Fail);
        
        return response;
    



  其中,需要使用到的配置文件内容:
#加密
encrypt.aes.key=b6eb4c32-6441-4b63-aae0-6a5cd7a46039
encrypt.aes.timeout=1
redis.key=aes:encrypt:key
redis.key.timeout=1

(4)最后,当然是模拟用户登录与注册的postman测试,看看效果:






   其中,注册返回的response的data中pass字段其实经过解密后的,可以从这段代码看出:
dataMap.put("pass",userService.decryptPassword(user.getPassword(),this.aesKey));

最后,当然是登录啦:从代码可以看出,登录是对request接收到的password进行加密,然后去数据库对应userName的加密串进行匹配,

如果相同,那就说明用户输入的密码是正确的(这也是目前大多数应用中“登录”逻辑的写法)

   
   好了,没什么介绍的了,总结一下:其实这种写法,在基于传统的固定key的安全写法上,变得更为安全,代码中隔一段时间更换
key的写法我觉得才是核心所在,实际应用中,你可以设置为一个月或者两个月动态更换一次,这个具体可以根据业务来定。最后
晒一下我经常变换的aesKey的记录表:


   就先介绍到这里吧,如果有问题可以加入群讨论: java开源技术交流:583522159 鏖战八方(开源群):391619659

以上是关于AES算法加密解密工具类util之改进之动态AES密钥加密的主要内容,如果未能解决你的问题,请参考以下文章

加密算法之AES

Java加密AES算法及spring中应用

对称加密算法(AES/ECB/PKCS5Padding)之ECB模式

对称加密算法(AES/ECB/PKCS5Padding)之ECB模式

逆向算法之AES算法

AES加密解密工具类封装(AESUtil)