前后端报文传输加密方案

前后端报文传输加密方案开发人员联系方式:251746034@qq.com代码库:https://github.com/chenjia/vue-desktop代码库:https://github.com/chenjia/vue-app代码库:https://github.com/chenjia/lxt示例:http://47.100.119.102/vue-desktop示例:http://47.100.119…

大家好,又见面了,我是你们的朋友全栈君。

开发人员联系方式:251746034@qq.com
代码库:https://github.com/chenjia/vue-desktop
代码库:https://github.com/chenjia/vue-app
代码库:https://github.com/chenjia/lxt
示例:http://47.100.119.102/vue-desktop
示例:http://47.100.119.102/vue-app
目的:前后端传输报文进行加密处理。

一、开发环境
前端技术:vue + axios
后端技术:java
加密算法:AES

为什么选择采用AES加密算法?作者在各种加密算法都进行过尝试,发现AES有以下特点比较符合要求:
1、加密解密执行速度快,相对DES更安全(原来采用的DES,结果部门的安全扫描建议用AES)
2、对称加密
3、被加密的明文长度可以很大,最多测试过10万长度的字符串。

java端AES加密示例,参考 lxt/lxt-common/com/lxt/ms/common/utils/SecurityUtils.java

public class SecurityUtils { 

public final static String letters = "abcdefghijklmnopqrstuvwxyz0123456789";
public final static String key = "ed26d4cd99aa11e5b8a4c89cdc776729";
private static String Algorithm = "AES";
private static String AlgorithmProvider = "AES/ECB/PKCS5Padding";
private final static String encoding = "UTF-8";
public static String encrypt(String src) throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException, InvalidAlgorithmParameterException { 

SecretKey secretKey = new SecretKeySpec(key.getBytes("utf-8"), Algorithm);
//IvParameterSpec ivParameterSpec = getIv();
Cipher cipher = Cipher.getInstance(AlgorithmProvider);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] cipherBytes = cipher.doFinal(src.getBytes(Charset.forName("utf-8")));
return Base64Utils.encodeToString(cipherBytes);
}
public static String decrypt(String src) throws Exception { 

SecretKey secretKey = new SecretKeySpec(key.getBytes("utf-8"), Algorithm);
//IvParameterSpec ivParameterSpec = getIv();
Cipher cipher = Cipher.getInstance(AlgorithmProvider);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] hexBytes = Base64Utils.decodeFromString(src);
byte[] plainBytes = cipher.doFinal(hexBytes);
return new String(plainBytes, "utf-8");
}
public static String md5Encrypt(String str) { 

try { 

MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[] byteDigest = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < byteDigest.length; offset++) { 

i = byteDigest[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
//32位加密
return buf.toString();
// 16位的加密
//return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) { 

e.printStackTrace();
return null;
}
}
public static String encryptKey(String key) throws Exception { 

String encryptedKey = "";
String[] array = key.split("");
Random random = new Random();
for (int i = 0; i < array.length; i++) { 

encryptedKey += array[i];
for (int j = 0; j < i % 2 + 1; j++) { 

int index = random.nextInt(letters.length());
encryptedKey += letters.substring(index, index + 1);
}
}
return Base64Utils.encodeToString(new StringBuilder(encryptedKey).reverse().toString().getBytes(encoding)).replaceAll("\n", "");
}
public static String decryptKey(String encryptedKey) { 

encryptedKey = new String(Base64Utils.decodeFromString(encryptedKey));
String key = "";
char[] c = new StringBuilder(encryptedKey).reverse().toString().toCharArray();
for (int i = 0, j = 0; i < encryptedKey.length(); i++) { 

key += c[i];
i += (j++ % 2 + 1);
}
return key;
}

前端AES加密,参考 vue-app/src/utils/security.js 或 vue-desktop/src/utils/security.js

var CryptoJS = require("crypto-js");
const encryptByAES = (message, key) => { 

var keyHex = CryptoJS.enc.Utf8.parse(key);
var encrypted = CryptoJS.AES.encrypt(message, keyHex, { 

mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64).replace(/[\r\n]/g, '');
}
const decryptByAES = (ciphertext, key) => { 

var keyHex = CryptoJS.enc.Utf8.parse(key);
var decrypted = CryptoJS.AES.decrypt({ 

ciphertext: CryptoJS.enc.Base64.parse(ciphertext.replace(/[\r\n]/g, ''))
}, keyHex, { 

mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
const encryptKey = key => { 

let array = key.split('')
let letters = 'abcdefghijklmnopqrstuvwxyz0123456789'
let encryptedKey = ''
for(let i=0;i<array.length;i++){ 

encryptedKey += array[i]
for(let j=0;j<i%2+1;j++){ 

encryptedKey += letters.substr(parseInt(Math.random()*letters.length),1)
}
}
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptedKey.split('').reverse().join('')))
}
const decryptKey = encryptedKey => { 

encryptedKey = CryptoJS.enc.Base64.parse(encryptedKey).toString(CryptoJS.enc.Utf8).split('').reverse().join('')
let str = ''
for(let i=0,j=0;i<encryptedKey.length;i++){ 

str += encryptedKey[i]
i += (j++ % 2 + 1)
}
return str
}
export { 
encryptByAES,decryptByAES,encryptKey,decryptKey}

好了,加密算法都有了,那怎么对报文进行加密呢?
前端利用axios的拦截器就可以轻松实现。

import axios from 'axios'
import cache from './cache'
import store from '../vuex/store'
import { 
encryptByAES,decryptByAES,encryptKey,decryptKey} from './security'
var CryptoJS = require("crypto-js");
window.axios = axios
let instance = axios.create({ 

method: 'post',
timeout: 60000,
withCredentials: true,
headers: { 

post: { 

'Content-Type': 'application/x-www-form-urlencoded'
}
},
transformRequest: [function(data) { 

let ret = ''
for (let it in data) { 

ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}]
})
instance.interceptors.request.use(function(config) { 

let user = cache.get('user')
let data = { 

head: { 

url: config.url,
debug: true,
userId: user ? user.userId : null,
token: cache.get('token'),
timestamp:new Date().getTime()
},
body: { 

data: config.data
}
}
console.log('\n【request:'+config.url+'】', data, '\n\n')
config.url = window.Config.server + config.url
config.data = { 

request: encryptByAES(JSON.stringify(data), decryptKey(Config.key))
}
return config
}, function(error) { 

console.log(error)
return Promise.reject(error)
})
instance.interceptors.response.use(function(response) { 

let resp = decryptByAES(response.data.response, decryptKey(Config.key))
response.data = JSON.parse(resp)
console.log('\n【response:'+response.config.url+'】',response, '\n\n')
if(response.data.head.status != 200){ 

store.commit('TOGGLE_POPUP', { 
visible: true, text: response.data.head.msg, duration: 3000})
}
let token = response.data.head.token
cache.set('token', token || cache.get('token'))
return response
}, function(error) { 

console.log(error)
return Promise.reject(error)
})
export default instance

注意上面 request 和 response 两个拦截器,在拦截 request 的时候,以下是对请求进行加密

config.data = {
request: encryptByAES(JSON.stringify(data), decryptKey(Config.key))
}

在拦截 response 的时候,以下是对响应的解密

let resp = decryptByAES(response.data.response, decryptKey(Config.key))
response.data = JSON.parse(resp)

这样,前端只要是通过 instance 这个模版发出去的请求,就能自动在请求时加密,响应时解密了。注意,这里的decryptKey(Config.key)是对进行简单混淆后的密钥进行反处理,才能得到最初的AES密钥。

前端部分好了,后台部分怎么做呢?其实思路都是类似的,后台是用的springcloud里面的zuul进行统一拦截的,当然你如果不是使用的微服务体系,后台通过最原始的过滤器也是可以的。

public class RequestFilter extends ZuulFilter{ 

@Value("#{'${filterUrls.services}'.split(',')}")
private String[] services;
@Value("${filterUrls.apis}")
private String apis;
@Value("#{'${filterUrls.excludes}'.split(',')}")
private String[] excludes;
@Override
public Object run() throws ZuulException{ 

RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
System.out.println("【contextPath】"+request.getContextPath());
System.out.println("【requestURI】"+request.getRequestURI());
String contextPath = request.getContextPath();
String uri = request.getRequestURI().replaceAll(contextPath, "");
String encryptedText = request.getParameter("request");
Packages pkg = new Packages();
String decryptedText = null;
try { 

decryptedText = SecurityUtils.decrypt(encryptedText);
pkg = JSONUtils.json2Obj(decryptedText, Packages.class);
} catch (Exception e) { 

e.printStackTrace();
pkg.getHead().setStatus(500);
pkg.getHead().setMsg("报文解密异常!");
}
if (pkg.getHead().getStatus() == 200 && apis.indexOf(uri) == -1) { 

String token = pkg.getHead().getToken();
String userId = pkg.getHead().getUserId();
if (StringUtils.isNotEmpty(userId)) { 

try { 

Map<String, Object> map = JWTUtils.parse(token);
if(userId.equals(map.get("userId"))){ 

Set<Object> resourceSet = CacheUtils.sGet("RESOURCE_"+userId);
if(resourceSet == null || !resourceSet.contains(uri)){ 

System.out.println("forbidden:"+uri);
}
// if(resourceSet == null || !resourceSet.contains(uri)){ 

// pkg.getHead().setStatus(500);
// pkg.getHead().setMsg("未授权的访问,请联系管理员!");
// }
}else { 

pkg.getHead().setStatus(500);
pkg.getHead().setMsg("token验证失败!");
}
} catch (Exception e) { 

e.printStackTrace();
pkg.getHead().setStatus(500);
pkg.getHead().setMsg("token转换失败!");
}
}
}
InputStream in = (InputStream) ctx.get("requestEntity");
if (in == null) { 

try { 

in = ctx.getRequest().getInputStream();
String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
body = "request=" + JSONUtils.obj2Json(pkg);
final byte[] reqBodyBytes = body.getBytes();
ctx.setRequest(new HttpServletRequestWrapper(ctx.getRequest()) { 

@Override
public ServletInputStream getInputStream() throws IOException { 

return new ServletInputStreamWrapper(reqBodyBytes);
}
@Override
public int getContentLength() { 

return reqBodyBytes.length;
}
@Override
public long getContentLengthLong() { 

return reqBodyBytes.length;
}
});
} catch (IOException e) { 

e.printStackTrace();
throw new ZuulException(e, 500, "获取输入流失败");
}
}
return null;
}
@Override
public boolean shouldFilter() { 

boolean shouldFilter = false;
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
String uri = request.getRequestURI();
for(String url : services){ 

if(uri.startsWith(url)){ 

shouldFilter = true;
break;
}
}
for(String exclude : excludes){ 

if(uri.startsWith(exclude)){ 

shouldFilter = false;
break;
}
}
return shouldFilter;
}
@Override
public int filterOrder() { 

return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
@Override
public String filterType() { 

return "pre";
}
}
public class ResponseFilter extends ZuulFilter { 

@Value("#{'${filterUrls.services}'.split(',')}")
private String[] services;
@Value("#{'${filterUrls.origins}'.split(',')}")
private Set<String> origins;
@Value("#{'${filterUrls.excludes}'.split(',')}")
private String[] excludes;
@Override
public Object run() throws ZuulException { 

RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
String origin = request.getHeader("Origin");
if (origins.contains(origin)) { 

response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Methods",
"POST,GET,OPTIONS");
response.setHeader("Access-Control-Allow-Headers",
"Origin,X-Requested-With,Content-Type,Accept,token");
response.setHeader("Access-Control-Allow-Credentials", "true");
}else { 

System.out.println("【origin】"+origin);
}
try { 

InputStream stream = ctx.getResponseDataStream();
String body = StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
String encryptedText = SecurityUtils.encrypt(body);
ctx.setResponseBody("{\"response\":\""+ encryptedText.replaceAll("\r\n|\n", "") +"\"}");
} catch (Exception e) { 

throw new ZuulException(e, 500, "报文加密异常");
}
return null;
}
@Override
public boolean shouldFilter() { 

boolean shouldFilter = false;
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
String uri = request.getRequestURI();
for(String url : services){ 

if(uri.startsWith(url)){ 

shouldFilter = true;
break;
}
}
for(String exclude : excludes){ 

if(uri.startsWith(exclude)){ 

shouldFilter = false;
break;
}
}
return shouldFilter;
}
@Override
public int filterOrder() { 

return FilterConstants.SEND_RESPONSE_FILTER_ORDER;
}
@Override
public String filterType() { 

return "post";
}
}
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication { 

public static void main(String[] args) { 

SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RequestFilter requestFilter() { 

return new RequestFilter();
}
@Bean
public ResponseFilter responseFilter() { 

return new ResponseFilter();
}
}

记得在启动类里面注册这两个过滤器(拦截器)。
作者的实现里面在拦截器里面加了大量的逻辑,可以根据自己的需要酌情删减。
比如:控制权限、控制需要拦截的接口前缀、控制拦截的例外。
再加上一个统一的熔断,可以更加友好的提醒前端。

@Component
public class FallbackConfig implements FallbackProvider { 

Logger logger = LoggerFactory.getLogger(FallbackConfig.class);
@Override
public String getRoute() { 

return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) { 

// if (cause != null && cause.getCause() != null) { 

// System.out.println(cause.getMessage());
// String reason = cause.getCause().getMessage();
// System.out.println("\n[fallback]"+reason+"\n");
// }
if(cause != null){ 

System.out.println("【fallback msg】"+cause.getMessage());
}
if (cause.getCause() != null) { 

System.out.println("【fallback cause】"+cause.getCause().getMessage());
}
return new ClientHttpResponse() { 

@Override
public HttpHeaders getHeaders() { 

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
@Override
public InputStream getBody() throws IOException { 

Packages pkg = new Packages();
pkg.getHead().setStatus(500);
pkg.getHead().setMsg("服务器正在开小差");
return new ByteArrayInputStream(JSONUtils.obj2Json(pkg).replace("\r\n", "").replace("\n", "").getBytes());
}
@Override
public String getStatusText() throws IOException { 

return "OK";
}
@Override
public HttpStatus getStatusCode() throws IOException { 

return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException { 

return 200;
}
@Override
public void close() { 

}
};
}
}

当前端某个接口调用异常的时候,后台统一返回提醒内容:服务器正在开小差,这样即使你的后台挂了,或者是在重启中(springcloud微服务重启单个服务很正常),前端都不会受影响。

最后提醒一句,任何前端加密都不能做到绝对的安全,毕竟代码都是暴露在浏览器的,特别是你的加密解密密钥,建议密钥也不要直明文暴露出来,而是对密钥进行简单的混淆处理后使用,再加上现在前后端都是分离的,前端一般都是es6或typescript使用webpack打包进行ugly处理,这样安全性也能提高不少。

好了,最后附上效果图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/145849.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号