前言:
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預設不支援jsp,是以我為了友善直接采用了thymeleaf模闆引擎,用法其實很簡單,有興趣的可以去學學
後記:
靜态頁面的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(參考)