框架中的自定义网关
背景:
有些公司项目中使用了自建的项目框架,网关进行了一些自定义的实现。如何实现一个自定义网关,又有哪些作用呢?本文将进行一些浅显的说明。
搭建一个项目 hand-gateway,并注册到nacos注册中心
项目依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置路由转发拦截器 RouteInterceptor
路由映射可以从配置文件、配置中心或者数据库中进行加载,然后做映射转换,然后通过RestTemplate进行转发,restTemplate通过loadblance可以实现负载均衡。下面代码实现了Post、get的方法请求,json请求暂未实现,感兴趣的可以试下。
@Slf4j
@Component
public class RouteInterceptor implements HandlerInterceptor {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
String requestUrl = request.getRequestURI();
if(StrUtil.isNotEmpty(request.getQueryString())){
String query = URLDecoder.decode(request.getQueryString(),"UTF-8");
requestUrl =requestUrl+"?"+query;
}
log.info("requestUrl {}",requestUrl);
// 注册中心获取注册的服务
// List<ServiceInstance> instances = discoveryClient.getInstances("paw-dogs-sky-service");
// String ipAddr = instances.get(0).getUri().toString();
// 加载路由映射 如配置文件、配置中心、redis、数据库
Map<String,String> routerMap = new HashMap<>(16);
routerMap.put("/sky-api","paw-dogs-sky-service");
String routerKey = requestUrl;
if(requestUrl.indexOf("/",1)>0){
routerKey = requestUrl.substring(0,requestUrl.indexOf("/",1));
}
if(routerMap.containsKey(routerKey)){
String serverName = routerMap.get(routerKey);
// 做地址映射
String targetUrl = "http://"+requestUrl.replaceFirst(routerKey,serverName);
log.info("requestUrl {} ==> targetUrl {}",requestUrl, targetUrl);
ResponseEntity<String> responseEntity;
if("POST".equalsIgnoreCase(request.getMethod())){
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
Map<String, String[]> paramMap = request.getParameterMap();
for (String paramName: paramMap.keySet()) {
params.addAll(paramName, Arrays.asList(paramMap.get(paramName)));
}
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<Object> requestEntity = new HttpEntity<>(params, httpHeaders);
responseEntity = restTemplate.postForEntity(targetUrl, requestEntity, String.class);
}else{
responseEntity = restTemplate.getForEntity(targetUrl, String.class);
}
try {
response.setStatus(responseEntity.getStatusCodeValue());
String responseBody = responseEntity.getBody();
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(responseBody);
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
return true;
}
@Override
public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
增加权限校验拦截器 TokenInterceptor,获取token后根据验证方式 jwt 或者redis的方式进行权限校验,并将用户的一些信息放入到header中,往下传播。
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
public static final String TOKEN_NAME = "token";
@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("TokenInterceptor requestUrl "+request.getRequestURI());
String token = request.getHeader(TOKEN_NAME);
if(StrUtil.isEmpty(token)){
token = request.getParameter(TOKEN_NAME);
}
// 校验token jwt 或者 redis 此处省略
if(StrUtil.isNotEmpty(token)){
return true;
}
try {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(HttpStatus.UNAUTHORIZED.getReasonPhrase());
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
配置服务,RestTemplate添加@LoadBalanced注解进行负载均衡,添加拦截器,注意添加的顺序。
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
@Autowired
private RouteInterceptor routeInterceptor;
@Bean
@LoadBalanced
@ConditionalOnClass(RestTemplate.class)
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/anonymous/**");
registry.addInterceptor(routeInterceptor).addPathPatterns("/**");
}
至此简单的网关项目完成。
搭建应用服务 sky-service,并注册到nacos注册中心
服务名称
spring:
application:
name: paw-dogs-sky-service
服务接口,增加port查看负载均衡,示例实现了get post方法
@RestController
public class SkyController {
@Value("${spring.application.name:sky-service}")
private String applicationName;
@Value("${server.port:8080}")
private Integer serverPort;
@GetMapping("/sky")
public String sky (@RequestParam(required = false) String name) {
String msg = "this is " + applicationName + " port " + serverPort + " Time: " + DateUtil.now();
if (StrUtil.isNotEmpty(name)) {
msg = msg + " name: " + name;
}
return msg;
}
@PostMapping("/deliver")
public String deliver (String packageBox) {
String msg = "this is " + applicationName + " port " + serverPort + " Time: " + DateUtil.now();
return msg + " delivered packageBox: " + packageBox;
}
}
通过idea的方式设置不同的端口启动两个服务,VM options 设置 -Dserver.port=8081
通过postman访问网关项目,header 携带token
http://127.0.0.1:8080/sky-api/sky?name=fly
返回结果
this is paw-dogs-sky-service port 8082 Time: 2021-06-17 16: 45: 05 name: fly
访问Post请求,header携带token,form-urlencoded参数 packageBox=cake
http://127.0.0.1:8080/sky-api/deliver
返回结果
this is paw-dogs-sky-service port 8082 Time: 2021-06-17 16: 11: 23 delivered packageBox: cake
多访问几次会发现 port端口在8081 8082切换,负载均衡实现。
至此一个自定义网关项目雏形完成,可以根据项目实际需要自定义其他内容,增加拦截器来实现不同的业务,如增加签名校验,对路由映射也可以做更复杂的业务,如接口是否需要签名校验,再如增加业务统一的内容放到header中,增加接口限流等。
总结:
自定义网关是一个spring-boot-web项目,基于RestTemplate进行路由转发,通过LoadBlance实现负载均衡,通过拦截器实现路由映射、token权限校验及其他业务内容。