开发人员联系方式: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处理,这样安全性也能提高不少。
好了,最后附上效果图: