文章目录
- 简述
- 整体效果
- 后端
- 增加配置
- nacos增加配置
- GlobalConfig
- 添加请求整体解密拦截器DecryptionFilter
- 添加响应整体解密拦截器EncryptionFilter
- 前端
- 请求拦截器
- 添加整体加密逻辑
- 请求头中添加sessionId
- 响应拦截器
- 添加整体解密逻辑
简述
本文网关gateway,微服务,vue已开源到gitee
杉极简/gateway网关阶段学习
在经历前面5章的铺垫下,终于进入到了最重要的环节——拦截器功能实现,此时我们需要看的东西就比较集中而且简单了。
在Java分布式系统中,使用Spring Cloud Gateway进行整体的加解密处理可以起到以下几个关键作用:
- 安全性增强:通过加密客户端与服务端之间传输的数据,可以防止敏感信息在网络传输过程中被截获和窃取。即使数据包被拦截,没有相应的密钥也无法解读数据内容,从而保护了用户隐私和系统安全。
- 数据完整性校验:在数据传输过程中,除了加密保护数据外,还可以通过签名机制来确保数据的完整性。签名可以验证数据在传输过程中是否被篡改,增强了数据的可靠性。
- 减轻后端服务负担:通过在网关层统一进行加解密处理,后端微服务不需要再集成额外的安全模块来进行数据的加解密工作,从而降低了后端服务的复杂性和资源消耗。
- 统一的安全管理:网关作为所有请求的入口点,可以实现统一的安全管理策略。所有的加解密操作按照统一的规则进行,便于管理和维护,同时方便对安全策略进行升级和维护。
- 提高开发效率:开发者可以专注于业务逻辑的实现,而不必关注数据传输的安全细节,提高了开发效率和项目的迭代速度。
- 跨服务数据保护:在微服务架构中,服务间可能需要相互调用和数据交换。网关加解密确保了即使在内部服务间传输的数据也是安全的,防止了潜在的内部安全风险。
- 应对合规性要求:某些行业或地区的法律法规要求对用户数据进行加密处理,使用网关加解密可以更好地符合这些合规性要求,避免法律风险。
- 灵活的策略调整:网关层的加解密策略可以根据业务需求灵活调整,比如可以针对不同的请求路径或请求方法应用不同的加密算法和密钥,而不需要修改每个微服务的代码。
- 减少敏感信息泄露风险:对于某些对数据安全性要求极高的应用场景,如金融交易、个人隐私信息等,网关加解密可以有效减少敏感信息泄露的风险。
- 提高系统的可扩展性:随着业务的发展,系统的安全需求可能会变化。在网关层实现加解密可以方便地根据新的安全需求进行扩展和更新,而不影响现有的业务流程和后端服务。
Spring Cloud Gateway的加解密功能为分布式系统提供了一个安全、高效、灵活的数据传输解决方案,有助于提升系统的安全性和开发效率。
整体效果
像不需要加密的接口,请求参数与响应数据都是未加密的
像数据接口,发送的参数是加密数据,接收到的都是是加密数据。但是经过前后端的拦截器处理之后,显示出的依然是正常数据
后端
增加配置
nacos增加配置
# 整体对称加解密
aes: true
# 整体非对称加解密
rsa: true
GlobalConfig
/**
* 整体对称加解密
*/
private boolean aes;
/**
* 整体非对称加解密
*/
private boolean rsa;
添加请求整体解密拦截器DecryptionFilter
package com.fir.gateway.filter.request;
import com.alibaba.fastjson.JSONObject;
import com.fir.gateway.config.GlobalConfig;
import com.fir.gateway.config.exception.CustomException;
import com.fir.gateway.config.result.AjaxStatus;
import com.fir.gateway.dto.ConnectDTO;
import com.fir.gateway.utils.AESUtils;
import com.fir.gateway.utils.RSAUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
;
/**
* 请求整理解密-请求拦截器
*
* @author fir
*/
@Slf4j
@Component
public class DecryptionFilter implements Ordered, GlobalFilter {
/**
* 网关参数配置
*/
@Resource
private GlobalConfig globalConfig;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public int getOrder() {
return -280;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("整体解密:start");
ServerHttpRequest req = exchange.getRequest();
String method = req.getMethodValue();
boolean rsa = globalConfig.isRsa();
boolean aes = globalConfig.isAes();
if (rsa || aes) {
// 白名单路由判断
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().toString();
List<String> whiteUrls = globalConfig.getWhiteUrls();
if(!whiteUrls.contains(path)){
ServerHttpRequest builder = req.mutate().build();
if (HttpMethod.GET.matches(method)) {
log.info("当前请求参数为: {}", req.getQueryParams());
builder = changeGet(exchange, builder);
} else if (HttpMethod.POST.matches(method)) {
log.info("当前请求参数为: {}", req.getQueryParams());
builder = changeGet(exchange, builder);
}
exchange = exchange.mutate().request(builder).build();
log.info("整体解密:true");
}else {
log.info("整体解密:true,白名单");
}
}else {
log.info("整体解密:true,验证已关闭");
}
return chain.filter(exchange);
}
/**
* 获取请求参数等信息进行过滤处理
*
* @param exchange 请求
* @param serverHttpRequest 请求
* @return 处理结束的参数
*/
@SneakyThrows
private ServerHttpRequest changeGet(ServerWebExchange exchange, ServerHttpRequest serverHttpRequest) {
String session = exchange.getRequest().getHeaders().getFirst("s");
if (session == null) {
throw new CustomException(AjaxStatus.SESSION_INVALID);
}
JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(session);
if (jsonObject == null) {
throw new CustomException(AjaxStatus.SESSION_EXPIRE);
}
ConnectDTO connectDTO = jsonObject.toJavaObject(ConnectDTO.class);
String privateKey = connectDTO.getPrivateKey();
String secretKey = connectDTO.getSecretKey();
// 获取原参数
URI uri = serverHttpRequest.getURI();
String originalQuery = uri.getRawQuery();
String decodedQuery = null;
if(StringUtils.isNotBlank(originalQuery)){
decodedQuery = URLDecoder.decode(originalQuery, "UTF-8");
}
// 更改参数
MultiValueMap<String, String> newQueryParams = new LinkedMultiValueMap<>();
if (StringUtils.isNotBlank(originalQuery) && org.springframework.util.StringUtils.hasText(decodedQuery)) {
// 修改请求参数,String[] array只能处理前端特定加密 {data:加密内容的形式}, 传递到后端,会变更为 data=加密内容。
// 除此以外的所有方式不能通过本方法进行解密
String[] array = decodedQuery.split("=");
if (array.length > 1) {
decodedQuery = array[1];
if (decodedQuery != null) {
boolean rsa = globalConfig.isRsa();
boolean aes = globalConfig.isAes();
if (rsa) {
// 对数据进行非对称解密
originalQuery = RSAUtils.decryptSection(decodedQuery, privateKey);
}
if (aes) {
// 对数据进行对称解密
originalQuery = AESUtils.decrypt(originalQuery, secretKey);
}
}
Map<String, Object> dataMap = JSONObject.parseObject(originalQuery, Map.class);
if (dataMap != null) {
Set<String> strings = dataMap.keySet();
for (String key : strings) {
String encodedString = URLEncoder.encode(dataMap.get(key).toString(), StandardCharsets.UTF_8.toString());
newQueryParams.add(key, encodedString);
}
}
}
}
// 替换查询参数
URI newUri = UriComponentsBuilder.fromUri(uri)
.query(null)
.queryParams(newQueryParams)
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
// 将解密后的参数重新设置到请求中
uri = request.getURI();
log.info("更改后的当前请求参数为: {}", uri.getRawQuery());
return request;
}
}
添加响应整体解密拦截器EncryptionFilter
package com.fir.gateway.filter.request;
import com.alibaba.fastjson.JSONObject;
import com.fir.gateway.config.GlobalConfig;
import com.fir.gateway.config.exception.CustomException;
import com.fir.gateway.config.result.AjaxStatus;
import com.fir.gateway.dto.ConnectDTO;
import com.fir.gateway.utils.AESUtils;
import com.fir.gateway.utils.RSAUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
;
/**
* 请求整理解密-请求拦截器
*
* @author fir
*/
@Slf4j
@Component
public class DecryptionFilter implements Ordered, GlobalFilter {
/**
* 网关参数配置
*/
@Resource
private GlobalConfig globalConfig;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public int getOrder() {
return -280;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("整体解密:start");
ServerHttpRequest req = exchange.getRequest();
String method = req.getMethodValue();
boolean rsa = globalConfig.isRsa();
boolean aes = globalConfig.isAes();
if (rsa || aes) {
// 白名单路由判断
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().toString();
List<String> whiteUrls = globalConfig.getWhiteUrls();
if(!whiteUrls.contains(path)){
ServerHttpRequest builder = req.mutate().build();
if (HttpMethod.GET.matches(method)) {
log.info("当前请求参数为: {}", req.getQueryParams());
builder = changeGet(exchange, builder);
} else if (HttpMethod.POST.matches(method)) {
log.info("当前请求参数为: {}", req.getQueryParams());
builder = changeGet(exchange, builder);
}
exchange = exchange.mutate().request(builder).build();
log.info("整体解密:true");
}else {
log.info("整体解密:true,白名单");
}
}else {
log.info("整体解密:true,验证已关闭");
}
return chain.filter(exchange);
}
/**
* 获取请求参数等信息进行过滤处理
*
* @param exchange 请求
* @param serverHttpRequest 请求
* @return 处理结束的参数
*/
@SneakyThrows
private ServerHttpRequest changeGet(ServerWebExchange exchange, ServerHttpRequest serverHttpRequest) {
String session = exchange.getRequest().getHeaders().getFirst("s");
if (session == null) {
throw new CustomException(AjaxStatus.SESSION_INVALID);
}
JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(session);
if (jsonObject == null) {
throw new CustomException(AjaxStatus.SESSION_EXPIRE);
}
ConnectDTO connectDTO = jsonObject.toJavaObject(ConnectDTO.class);
String privateKey = connectDTO.getPrivateKey();
String secretKey = connectDTO.getSecretKey();
// 获取原参数
URI uri = serverHttpRequest.getURI();
String originalQuery = uri.getRawQuery();
String decodedQuery = null;
if(StringUtils.isNotBlank(originalQuery)){
decodedQuery = URLDecoder.decode(originalQuery, "UTF-8");
}
// 更改参数
MultiValueMap<String, String> newQueryParams = new LinkedMultiValueMap<>();
if (StringUtils.isNotBlank(originalQuery) && org.springframework.util.StringUtils.hasText(decodedQuery)) {
// 修改请求参数,String[] array只能处理前端特定加密 {data:加密内容的形式}, 传递到后端,会变更为 data=加密内容。
// 除此以外的所有方式不能通过本方法进行解密
String[] array = decodedQuery.split("=");
if (array.length > 1) {
decodedQuery = array[1];
if (decodedQuery != null) {
boolean rsa = globalConfig.isRsa();
boolean aes = globalConfig.isAes();
if (rsa) {
// 对数据进行非对称解密
originalQuery = RSAUtils.decryptSection(decodedQuery, privateKey);
}
if (aes) {
// 对数据进行对称解密
originalQuery = AESUtils.decrypt(originalQuery, secretKey);
}
}
Map<String, Object> dataMap = JSONObject.parseObject(originalQuery, Map.class);
if (dataMap != null) {
Set<String> strings = dataMap.keySet();
for (String key : strings) {
String encodedString = URLEncoder.encode(dataMap.get(key).toString(), StandardCharsets.UTF_8.toString());
newQueryParams.add(key, encodedString);
}
}
}
}
// 替换查询参数
URI newUri = UriComponentsBuilder.fromUri(uri)
.query(null)
.queryParams(newQueryParams)
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
// 将解密后的参数重新设置到请求中
uri = request.getURI();
log.info("更改后的当前请求参数为: {}", uri.getRawQuery());
return request;
}
}
前端
请求拦截器
添加整体加密逻辑
// 请求整体加密
if (AESKey) {
const secretKey = this.get("secretKey");
let date = JSON.stringify(request.params);
date = this.encryptAES(date, secretKey);
request.params = {"data": date};
}
if (AESKey && RSAKey) {
const serverPublicKey = this.get("serverPublicKey");
let date = request.params.data;
date = this.rsaEncrypt(date, serverPublicKey);
request.params = {"data": date};
} else if (RSAKey) {
const serverPublicKey = this.get("serverPublicKey");
let date = JSON.stringify(request.params);
date = this.rsaEncrypt(date, serverPublicKey);
request.params = {"data": date};
}
请求头中添加sessionId
let s = this.get("sessionId")
// 请求中增加会话信息
if (s) {
request.headers.s = s;
}
响应拦截器
添加整体解密逻辑
修改一下内容:
return securityUtils.gatewayResponse(response);
securityUtils.js中添加响应处理函数
/**
* gateway网关验证信息处理(响应头)
*/
gatewayResponse(response) {
let key = true;
// 放置业务逻辑代码
// response是服务器端返回来的数据信息,与Promise获得数据一致
let data = response.data
// config包含请求信息
let config = response.config
// 判断 data 是否为对象
if (typeof data === 'object' && data !== null) {
// 判断 data 是否匹配特定格式
if (
Object.prototype.hasOwnProperty.call(data, 'msg') &&
Object.prototype.hasOwnProperty.call(data, 'code') &&
typeof data.msg === 'string' &&
typeof data.code === 'number'
) {
// 数据匹配特定格式
if (data.code === 401) {
sessionStorage.clear()
}
return data;
}
}
// 获取当前请求的url
let url = config.url
whiteList.find(function (value) {
if (value === url) {
key = false;
}
});
// 对非白名单数据进行整体解密处理
if (key) {
// 获取加密密钥,并传入解密组件进行解密
if (RSAKey) {
const privateKey = this.get("privateKey")
data = this.rsaDecrypt(data, privateKey)
}
if (AESKey) {
let securityKey = this.get("secretKey")
data = this.decryptAES(data, securityKey)
}
if (data != null && data !== "") {
data = JSON.parse(data);
}else {
data = new Promise(() => {});
}
}
return data;
},