大家好,又见面了,我是你们的朋友全栈君。
开发人员联系方式: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账号...