前后端报文传输加密方案

前后端报文传输加密方案开发人员联系方式 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

开发人员联系方式: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
encryptedKey += array[i]
for(let j=0;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
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 map = JWTUtils.parse(token);
if(userId.equals(map.get("userId"))){

Set 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 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处理,这样安全性也能提高不少。

好了,最后附上效果图:

编程小号
上一篇 2025-03-17 14:01
下一篇 2025-08-26 18:57

相关推荐

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