天天看點

學習加密(二)Spring Boot 使用AES對稱加密,前後端傳遞參數加解密

前言:

1.最近要做一個安全性稍微高一點的項目,首先就想到了要對參數加密,和采用https協定.

2.以前對加密這塊不了解,查閱了很多資料,加密方式很多種,但是大概區分兩種,一個就是對稱加密(DES,3DES,AES,IDEA等),另外一個就是非對稱加密(RSA,Elgamal,背包算法,Rabin,D-H等)

3.這兩種差別還是有的,粗淺的說:

 (1)對稱加密方式效率高,但是有洩露風險

 (2)非對稱加密方式效率比對稱加密方式效率低,但是基本上沒有洩露風險

4.如果想了解加密的,請先看我整理的另外一篇文章:https://blog.csdn.net/baidu_38990811/article/details/83386312

使用對稱加密方式(AES)實踐:

1.建立spring boot項目,導入相關依賴

2.編寫加密工具類

3.編寫自定義注解(讓加解密細粒度)

4.編寫自定義DecodeRequestAdvice和EncodeResponseBodyAdvice

5.建立controller

6.建立jsp或者html,引入js(加密和解密的通用js)

ps:因為這裡沒https證書,所有使用http, 考慮到前後端分離,使用json來傳遞資料

第一步: 略,不會的請自行百度spring boot項目如何建立!

第二步:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.apache.commons.codec.binary.Base64;

import java.util.HashMap;
import java.util.Map;

/**
 * 前後端資料傳輸加密工具類
 * @author monkey
 *
 */
public class AesEncryptUtils {
    //可配置到Constant中,并讀取配置檔案注入,16位,自己定義
    private static final String KEY = "xxxxxxxxxxxxxxxx";

    //參數分别代表 算法名稱/加密模式/資料填充方式
    private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";

    /**
     * 加密
     * @param content 加密的字元串
     * @param encryptKey key值
     * @return
     * @throws Exception
     */
    public static String encrypt(String content, String encryptKey) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        kgen.init(128);
        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
        byte[] b = cipher.doFinal(content.getBytes("utf-8"));
        // 采用base64算法進行轉碼,避免出現中文亂碼
        return Base64.encodeBase64String(b);

    }

    /**
     * 解密
     * @param encryptStr 解密的字元串
     * @param decryptKey 解密的key值
     * @return
     * @throws Exception
     */
    public static String decrypt(String encryptStr, String decryptKey) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        kgen.init(128);
        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));
        // 采用base64算法進行轉碼,避免出現中文亂碼
        byte[] encryptBytes = Base64.decodeBase64(encryptStr);
        byte[] decryptBytes = cipher.doFinal(encryptBytes);
        return new String(decryptBytes);
    }

    public static String encrypt(String content) throws Exception {
        return encrypt(content, KEY);
    }
    public static String decrypt(String encryptStr) throws Exception {
        return decrypt(encryptStr, KEY);
    }


    public static void main(String[] args) throws Exception {
        Map map=new HashMap<String,String>();
        map.put("key","value");
        map.put("中文","漢字");
        String content = JSONObject.toJSONString(map);
        System.out.println("加密前:" + content);

        String encrypt = encrypt(content, KEY);
        System.out.println("加密後:" + encrypt);

        String decrypt = decrypt(encrypt, KEY);
        System.out.println("解密後:" + decrypt);
    }
}
           

第三步:

import org.springframework.web.bind.annotation.Mapping;

import java.lang.annotation.*;


/**
 * @author monkey
 * @desc 請求資料解密
 * @date 2018/10/25 20:17
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Mapping
@Documented
public @interface SecurityParameter {

    /**
     * 入參是否解密,預設解密
     */
    boolean inDecode() default true;

    /**
     * 出參是否加密,預設加密
     */
    boolean outEncode() default true;
}
           

第四步:

DecodeRequestAdvice類

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * @author monkey
 * @desc 請求資料解密
 * @date 2018/10/25 20:17
 */
@ControllerAdvice(basePackages = "com.xxx.springboot.demo.controller")
public class DecodeRequestBodyAdvice implements RequestBodyAdvice {

    private static final Logger logger = LoggerFactory.getLogger(DecodeRequestBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        try {
            boolean encode = false;
            if (methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class)) {
                //擷取注解配置的包含和去除字段
                SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
                //入參是否需要解密
                encode = serializedField.inDecode();
            }
            if (encode) {
                logger.info("對方法method :【" + methodParameter.getMethod().getName() + "】傳回資料進行解密");
                return new MyHttpInputMessage(inputMessage);
            }else{
                return inputMessage;
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("對方法method :【" + methodParameter.getMethod().getName() + "】傳回資料進行解密出現異常:"+e.getMessage());
            return inputMessage;
        }
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    class MyHttpInputMessage implements HttpInputMessage {
        private HttpHeaders headers;

        private InputStream body;

        public MyHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
            this.headers = inputMessage.getHeaders();
            this.body = IOUtils.toInputStream(AesEncryptUtils.decrypt(easpString(IOUtils.toString(inputMessage.getBody(), "UTF-8"))), "UTF-8");
        }

        @Override
        public InputStream getBody() throws IOException {
            return body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }

        /**
         *
         * @param requestData
         * @return
         */
        public String easpString(String requestData){
            if(requestData != null && !requestData.equals("")){
                String s = "{\"requestData\":";
                if(!requestData.startsWith(s)){
                    throw new RuntimeException("參數【requestData】缺失異常!");
                }else{
                    int closeLen = requestData.length()-1;
                    int openLen = "{\"requestData\":".length();
                    String substring = StringUtils.substring(requestData, openLen, closeLen);
                    return substring;
                }
            }
            return "";
        }
    }
           

EncodeResponseAdvice類:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @author monkey
 * @desc 傳回資料加密
 * @date 2018/10/25 20:17
 */
@ControllerAdvice(basePackages = "com.xxx.springboot.demo.controller")
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {

    private final static Logger logger = LoggerFactory.getLogger(EncodeResponseBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        boolean encode = false;
        if (methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class)) {
            //擷取注解配置的包含和去除字段
            SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
            //出參是否需要加密
            encode = serializedField.outEncode();
        }
        if (encode) {
            logger.info("對方法method :【" + methodParameter.getMethod().getName() + "】傳回資料進行加密");
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
                return AesEncryptUtils.encrypt(result);
            } catch (Exception e) {
                e.printStackTrace();
                logger.error("對方法method :【" + methodParameter.getMethod().getName() + "】傳回資料進行解密出現異常:"+e.getMessage());
            }
        }
        return body;
    }
}
           

第五步:

@Controller
public class TestController {

    /*
     * 測試傳回資料,會自動加密
     * @return
     */
    @GetMapping("/get")
    @ResponseBody
    @SecurityParameter
    public Object get() {
        Persion info = new Persion();
        info.setName("好看");
        return info;
    }
    /*
     * 自動解密,并将傳回資訊加密
     * @param info
     * @return
     */
    @RequestMapping("/save")
    @ResponseBody
    @SecurityParameter
    public Object save(@RequestBody Persion info) {
        System.out.println(info.getName());
        return info;
    }

}
           

第六步:

引入js檔案:位址https://download.csdn.net/download/baidu_38990811/10744745

學習加密(二)Spring Boot 使用AES對稱加密,前後端傳遞參數加解密

由于spring boot預設不支援jsp,是以我為了友善直接采用了thymeleaf模闆引擎,用法其實很簡單,有興趣的可以去學學

學習加密(二)Spring Boot 使用AES對稱加密,前後端傳遞參數加解密

後記:

靜态頁面的return預設是跳轉到/static/index.html,當在pom.xml中引入了thymeleaf元件,動态跳轉會覆寫預設的靜态跳轉,預設就會跳轉到/templates/index.html,是以隻需要通路http://localhost:8080就可以了,動态沒有html字尾。

整個流程,我已經測試過了,完全沒問題,隻不過,采用對稱加密,秘鑰相同,前後端都需要拿到秘鑰才能進行加解密,這樣就有秘鑰洩露的風險了,服務端還好說一點,安全稍微高點,但是前端就比較有風險,是以前端要考慮,js進行混淆和加密,當然隻能讓風險降低,也不是完全沒風險了!!!

後續可以考慮: 對稱加密和非對稱加密聯合使用來進行加密,各取長處,但是代碼要複雜些!

參考文章:

1. https://github.com/yinjihuan/spring-boot-starter-encrypt (仔細看)

2.https://blog.csdn.net/qq_32079585/article/details/77867097 (可以快速看)

3.https://blog.csdn.net/junmoxi/article/details/80917234?utm_source=blogxgwz2 (複雜的方式,不過擴充的好)

4.https://blog.csdn.net/huang812561/article/details/79424041?utm_source=blogxgwz1(參考)

繼續閱讀