项目搭建工具及版本:
eclipse / jdk1.8 / springboot2.5.0
实现功能:
应用场景:一般来说我们前后端交互或者请求和服务方交互会对报文进行加密操作,为了实现这个功能,我们将通过下面的思路完成这个功能的实现:
1.准备好加解密的工具类AES、3DES等等,还有编码Base64工具类;
2.通过spring的切面,也就是在请求和响应层级对整个请求和响应的报文实体进行加密解密操作;
3.自定义注解,实现将来每一个controller的方法上但凡有这个注解就需要加解密功能;
4.配置文件中加入加解密的开关,控制是否开启加解密功能。
1.配置文件常量
#是否启用报文加密机制
packet-encryption.open=false
packet-encryption.charset=utf-8
2.加解密目录所需功能类及说明
advice目录: 切面处理请求和响应的报文体加解密(目前作为示例使用base64编码进行演示)
annotation目录:自定义注解(有该注解的方法就会被执行加解密操作)
auto:加解密支持类和配置类
utils:工具类
3.源代码
package com.bbnet.common.encrypt.advice;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 com.bbnet.common.encrypt.annotation.Decrypt;
import com.bbnet.common.encrypt.auto.EncryptProperties;
import com.bbnet.common.encrypt.utils.BASE64Util;
/**
* @author dym
* @Description 请求数据接收处理 对加了@Decrypt的方法的数据进行解密操作 只对@RequestBody参数有效
* @date 2020/4/20 10:26
*
* @updateBy sgc
*/
@ControllerAdvice
public class EncryptRequestBodyAdvice implements RequestBodyAdvice {
private Logger logger = LoggerFactory.getLogger(EncryptRequestBodyAdvice.class);
@Autowired
private EncryptProperties encryptProperties;
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
if (parameter.getMethod().isAnnotationPresent(Decrypt.class) && encryptProperties.isOpen()){
try {
return new DecryptHttpInputMessage(inputMessage,encryptProperties.getCharset());
}catch (Exception e){
logger.error("数据解密失败",e);
}
}
return inputMessage;
}
public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return o;
}
public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return o;
}
class DecryptHttpInputMessage implements HttpInputMessage {
private Logger logger = LoggerFactory.getLogger(DecryptHttpInputMessage.class);
private HttpHeaders httpHeaders;
private InputStream body;
public DecryptHttpInputMessage(HttpInputMessage inputMessage, String charset) throws Exception{
this.httpHeaders = inputMessage.getHeaders();
String content = IOUtils.toString(inputMessage.getBody(), charset);
long startTime = System.currentTimeMillis();
//JSON数据格式的不进行解密操作
String decryptBody = "";
if (content.startsWith("{")){
decryptBody = content;
}else {
//2021-03-30 add by sgc 增加算法加密逻辑
decryptBody = BASE64Util.decode(content);
// decryptBody = BASE64Util.decodeHzsun(content);
}
// logger.info("请求报文解密前:"+ content);
logger.info("请求报文解密后:\n"+ decryptBody);
long endTime = System.currentTimeMillis();
logger.debug("请求报文解密耗时:" + (startTime - endTime));
this.body = IOUtils.toInputStream(decryptBody,charset);
}
public InputStream getBody() throws IOException {
return body;
}
public HttpHeaders getHeaders() {
return httpHeaders;
}
}
}
package com.bbnet.common.encrypt.advice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
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;
import com.bbnet.common.encrypt.annotation.Encrypt;
import com.bbnet.common.encrypt.auto.EncryptProperties;
import com.bbnet.common.encrypt.utils.BASE64Util;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author dym
* @Description 请求响应处理类 对加了@Encrypt的方法的数据进行加密操作
* @date 2020/4/20 10:41
*
* @updateBy sgc
*/
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private Logger logger = LoggerFactory.getLogger(EncryptResponseBodyAdvice.class);
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private EncryptProperties encryptProperties;
private static ThreadLocal<Boolean> encryptLocal = new ThreadLocal<Boolean>();
public static void setEncryptStatus(boolean status){
encryptLocal.set(status);
}
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
//可以通过调用EncryptResponseBodyAdvice.setEncryptStatus(false)来动态设置不加密操作
Boolean status = encryptLocal.get();
if (status != null && status == false){
encryptLocal.remove();
return body;
}
long startTime = System.currentTimeMillis();
boolean encrypt = false;
if (returnType.getMethod().isAnnotationPresent(Encrypt.class) && encryptProperties.isOpen()){
encrypt = true;
}
if (encrypt){
try{
String content = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
logger.info("响应报文加密前:\n"+content);
//开始用base64编码
//2021-03-24 add by sgc 增加算法加密逻辑
String result = BASE64Util.encode(content).replace("\r\n","");
// String result = BASE64Util.encodeHzsun(content).replace("\r\n","");
// logger.info("响应报文加密后:"+result);
long endTime = System.currentTimeMillis();
logger.debug("响应报文加密耗时:" + (startTime - endTime));
return result;
}catch (Exception e){
logger.error("加密数据异常",e);
}
}
return body;
}
}
package com.bbnet.common.encrypt.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author dym
* @Description 解密注解 加了此注解的接口将进行数据解密操作
* @date 2020/4/20 10:29
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {
@SuppressWarnings("rawtypes")
Class[] value() ;
}
package com.bbnet.common.encrypt.annotation;
import java.lang.annotation.*;
/**
* @Description 加密注解 加了此注解的接口将进行数据加密操作
* @author dym
* @date 2020/4/20 10:32
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypt {
}
package com.bbnet.common.encrypt.auto;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* @author dym
* @Description 加密解密自动配置
* @date 2020/4/20 10:40
*/
@Configuration
@Component
@EnableAutoConfiguration
@EnableConfigurationProperties(EncryptProperties.class)
public class EncryptAuroConfiguration {
}
package com.bbnet.common.encrypt.auto;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author dym
* @Description 加密配置类,此处定义了编码和是否调试模式等等的设置,后续如果要采用其他加密方式,可以在这里设置加密密钥和过期时间等等
* @date 2020/4/20 10:35
*/
@ConfigurationProperties(prefix = "packet-encryption")
public class EncryptProperties {
private String charset ;
/**
* 开启调试模式,调试模式下不进行加解密操作,用于像Swagger这种在线API测试场景
*/
private boolean open;
public String getCharset() {
return charset;
}
public void setCharset(String charset) {
this.charset = charset;
}
public boolean isOpen() {
return open;
}
public void setOpen(boolean open) {
this.open = open;
}
}
package com.bbnet.common.encrypt.utils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/**
* @author dym
* @Description base64工具类
* @date 2020/4/17 16:37
*
* @updateNote 增加项目自定义加密逻辑
* @updateBy sgc
* @updateDate 2021-03-27
*/
@SuppressWarnings("restriction")
public final class BASE64Util {
/**串前面加几位的字符*/
public static final int prefixSize = 13;
/**串中间从几位开始加*/
public static final int middleStartIndex = 1;
/**串中间加几位的字符*/
public static final int middleSize = 1;
/**串后面加几位的字符*/
public static final int tailSize = 6;
//自测
public static void main(String[] args) {
String cs = "1";
String basecs = null;
String basecsde = "";
basecs = encode(cs);
System.out.println("编码:"+basecs);
basecsde = decode(basecs);
System.out.println("解码:"+basecsde);
String csss = encodeHzsun(cs);
System.out.println(csss);
System.out.println(decodeHzsun(csss));
}
/**
* @Description 改造之后的base64编码
*
* @param base
* @return
*
* @author sgc
* @date 2021-03-30
*/
public static final String encodeHzsun(String base){
if(base==null || base.equals("")) {
return encode(base);
}
String oldStr = encode(base);
//增加两头的串
// String preStr = oldStr.substring(0, middleStartIndex);
// String lastStr = oldStr.substring(middleStartIndex, oldStr.length());
// String newSytr = RandomUtil.getItemID(prefixSize) + preStr + RandomUtil.getItemID(middleSize) + lastStr + RandomUtil.getItemID(tailSize);
String newSytr = RandomUtil.getItemID(prefixSize) + oldStr + RandomUtil.getItemID(tailSize);
return newSytr;
}
/**
* @Description 改造之后的base64解码
*
* @param encoder
* @return
*
* @author sgc
* @date 2021-03-30
*/
public static final String decodeHzsun(String encoder){
if(encoder.length() <= (prefixSize+tailSize)) {
return decode(encoder);
}
//先取掉两头的串,重新拼接串进行解码
String oldStr = "";
oldStr = encoder.substring(prefixSize, (encoder.length()-tailSize));
// String preStr = oldStr.substring(0, middleStartIndex);
// String lastStr = oldStr.substring(middleStartIndex+1, oldStr.length());
// String newStr = preStr+lastStr;
// return decode(newStr);
return decode(oldStr);
}
/**
* 采用BASE64算法对字符串进行加密
* @param base 原字符串
* @return 加密后的字符串
*/
public static final String encode(String base){
return BASE64Util.encode(base.getBytes());
}
/**
* 采用BASE64算法对字节数组进行加密
* @param baseBuff 原字节数组
* @return 加密后的字符串
*/
public static final String encode(byte[] baseBuff){
return new BASE64Encoder().encode(baseBuff);
}
/**
* 字符串解密,采用BASE64的算法
* @param encoder 需要解密的字符串
* @return 解密后的字符串
*/
public static final String decode(String encoder){
try {
BASE64Decoder decoder = new BASE64Decoder();
byte[] buf = decoder.decodeBuffer(encoder);
return new String(buf);
} catch (Exception e) {
return null;
}
}
}
4.测试
如下面的部分代码,该方法我们加入了@Encrypt和@Decrypt(value = { Token.class })注解,意味着该方法需要解密传入报文,加密返回报文。
//code...
@SuppressWarnings("rawtypes")
@ApiOperation(value="删除token接口", notes="token时效情况下删除token,即类似于退出登录")
@Encrypt
@Decrypt(value = { Token.class })
@PostMapping("/removeToken")
public ResponseResult removeToken(@RequestBody Token token) {
//参数校验
if(oConvertUtils.isEmpty(token.getAccount())) {
throw new ServiceException(ErrorEnum.RESP_CODE_FAIL.getCode(), "账号或用户名为空");
}
//account的加密结果
String accountEnc = AES.aesEncryptBase64(token.getAccount());
//bbnet:user_prefix_token:002604_MnJMUnY1TWNhVmZsMXlLQjNkajR6QT09
String key = ServiceConstants.TOKEN_PREFIX+token.getAccount()+"_"+accountEnc;
log.info("Token的key为:{}", key);
//如果已经存在token直接返回
if(redisService.hasKey(key)) {
redisService.del(key);
}
return ResponseUtils.success();
}
//code...
package com.bbnet.common.base.token;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
@ApiModel(value="TOKEN报文实体")
public class Token {
@ApiModelProperty(value = "账号/用户名", dataType = "String")
String account;
@ApiModelProperty(value = "口令/密码", dataType = "String")
String password;
}
比如以下请求示例截图:
补充:整个教程中所有代码都是连贯的,可能存在部分代码中有报错的代码块,大家可以自行甄别,或者艾特我,目前本次搭建的框架已经在使用中,如有需要可以加入shiro的机制,因为想做一个简单纯净的springboot后端框架座位后端中间件的服务,不作为web端的后端服务,所以并未集成一些别的大型的更安全的工具,仅供学习交流使用嘛。