1、为什么使用网关
微服务架构体系中,服务数量较多,独立部署提供给外部时,一方面暴露了服务内部细节,另外一方面也不方便管理,例如:nginx做负载均衡时需要管理较多的服务信息。
2、网关的作用
网关作为外部调用服务的统一入口,可以做到用户身份验证、监控、负载均衡、限流、降级与应用检测等功能。
【黑名单】:通过IP地址或者解析用户token,根据用户信息来控制禁止访问实际应用服务
【Token验证】:进行token校验拦截掉无效或失效的token请求
【路由】:路由转发作为网关核心功能,客户端通过nginx统一反向代理至网关服务,网关可以从zk服务注册中心拿到所有应用服务的实际地址,再进行rpc远程调用,拿到的服务地址已经是进行轮询算法后的
【日志】实现访问日志的记录,可用于分析访问、处理性能指标
3、主要实现
本文中介绍的网关服务,集成了zk服务注册与发现,见zookeeper使用篇(二):服务注册与发现+本地负载均衡
/**
* 身份验证过滤器
*/
@Component
@WebFilter(urlPatterns = {"/*"}, filterName = "authFilter")
@NoArgsConstructor
@Slf4j
public class AuthFilter implements Filter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private JedisUtil jedisUtil;
@Autowired
private AuthMapper authMapper;
private LoadBalanse loadBalanse = new RandomLoadBalance();
public void init(FilterConfig filterConfig) {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE,PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
HttpServletRequest request = (HttpServletRequest) req;
// 禁止/favicon.ico图标请求
if ("/favicon.ico".equals(request.getRequestURI())) {
return;
}
// 先进行用户验证,成功则转发请求
ResultInfo resultInfo = checkUser(request);
if (resultInfo.getCode() == 200) {
// 请求转发
forwardRequest(request, response);
return;
}
response.sendError(resultInfo.getCode(), resultInfo.getMessage());
} finally {
UserContext.clean();
}
}
/**
* 用户验证流程:
* 第一步、拦截黑名单(根据ip查询数据库配置,级别:ip、project、route)
* 第二步、匿名访问,则可跳过token验证环节
* 第三步、验证token
* 第四步、验证成功,进行路由转发
*/
private ResultInfo checkUser(HttpServletRequest req) {
ResultInfo resultInfo = new ResultInfo();
// 1、判断是否为黑名单
if (checkBlackList(req)) {
resultInfo.setCode(403);
resultInfo.setMessage("您已被列入黑名单!");
} else {
// 2、判断是否可以匿名访问,跳过token验证环节
if (authMapper.ifFilterUrl(req.getRequestURI())) {
resultInfo.setCode(200);
resultInfo.setMessage("用户授权认证通过!");
} else {
// 3、否则,进行token验证
resultInfo = checkToken(req);
}
}
return resultInfo;
}
//黑名单验证
private Boolean checkBlackList(HttpServletRequest req) {
String token = req.getHeader("Authorization");
String emailAddress = jwtTokenUtil.parseJWT(token).getSubject();
String realIP = IPUtils.getRealIP(req);
log.info("客户端真实ip地址信息:" + realIP);
BlackListInfo blackListInfo = new BlackListInfo();
blackListInfo.setEmail(emailAddress);
blackListInfo.setIp(realIP);
//黑名单级别判断:ip/email、project、route
List<BlackListInfo> blackList = authMapper.getBlackList(blackListInfo);
List<BlackListInfo> ipBlackList;
List<BlackListInfo> proBlackList;
List<BlackListInfo> routeBlackList;
if (blackList.size() != 0) {
ipBlackList = blackList.stream().filter(o -> o.ifUserForbidden()).collect(Collectors.toList());
if (ipBlackList.size() == 0) {
String uri = req.getRequestURI();
int index = StringUtils.ordinalIndexOf(uri, "/", 2);
String projectName = uri.substring(1, index);
String routePath = uri.substring(index);
proBlackList = blackList.stream().filter(o -> o.ifProForbidden()).collect(Collectors.toList());
for (BlackListInfo black : proBlackList) {
if (projectName.equals(black.getProjectName())) {
return true;
}
}
routeBlackList = blackList.stream().filter(o -> o.ifRouteForbidden()).collect(Collectors.toList());
for (BlackListInfo black : routeBlackList) {
if (routePath.equals(black.getRoutePath())) {
return true;
}
}
} else {
return true;
}
}
return false;
}
//token验证
private ResultInfo checkToken(HttpServletRequest request) {
ResultInfo resultInfo = new ResultInfo();
String token = request.getHeader("Authorization");
log.info("token:" + token);
if (null != token && !token.isEmpty()) {
try {
String emailAddress = jwtTokenUtil.parseJWT(token).getSubject();
log.info("email:" + emailAddress);
if (this.jedisUtil.get(emailAddress) != null && this.jedisUtil.get(emailAddress).equals(token.replace("Bearer ", "")) && this.jwtTokenUtil.validateToken(token)) {
resultInfo.setCode(200);
this.setAuthorization(request);
resultInfo.setMessage("用户授权认证通过!");
} else {
resultInfo.setCode(401);
resultInfo.setMessage("用户授权认证没有通过!");
}
} catch (ExpiredJwtException | MalformedJwtException var6) {
resultInfo.setCode(401);
resultInfo.setMessage("用户令牌信息有误,认证不通过!");
}
} else {
resultInfo.setCode(401);
resultInfo.setMessage("用户授权认证没有通过!客户端请求参数中无token信息");
}
return resultInfo;
}
//设置用户信息
private void setAuthorization(HttpServletRequest request) {
String token = request.getHeader("Authorization");
UserContext.setAuthorization(token);
String emailAddress = this.jwtTokenUtil.parseJWT(token).getSubject();
UserContext.setEmailAddress(emailAddress);
}
//请求转发
private void forwardRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String uri = request.getRequestURI();
int index = StringUtils.ordinalIndexOf(uri, "/", 2);
if (index > -1) {
String service = uri.substring(1, index);
String host;
try {
host = loadBalanse.getServerUrl(service);
if (StringUtils.isEmpty(host)) {
throw new RuntimeException("请求异常,请检查服务信息");
}
} catch (Exception e) {
log.error(e.getMessage() + e);
throw new RuntimeException("请求异常,请检查服务信息");
}
String servletPath = uri.substring(index);
String forward = "http://" + host + servletPath;
switch (request.getMethod()) {
case "GET": {
RestTemplateUtil.requestService(request, response, HttpMethod.GET, forward);
break;
}
case "POST": {
RestTemplateUtil.requestService(request, response, HttpMethod.POST, forward);
break;
}
case "PUT": {
RestTemplateUtil.requestService(request, response, HttpMethod.PUT, forward);
break;
}
case "PATCH": {
RestTemplateUtil.requestService(request, response, HttpMethod.PATCH, forward);
break;
}
case "DELETE": {
RestTemplateUtil.requestService(request, response, HttpMethod.DELETE, forward);
break;
}
default: {
log.error("unknow request method:" + request.getMethod());
throw new RuntimeException("请求异常,请求方法未知");
}
}
}
}
public void destroy() {
}
}
@Slf4j
public class RestTemplateUtil {
private static final RestTemplate restTemplate = new RestTemplate();
private static long MAX_SIZE = 10 * 1024 * 1024 * 1024;// 设置上传文件最大为 10G
public static void requestService(HttpServletRequest req, HttpServletResponse rsp, HttpMethod method, String uri) throws ServletException, IOException {
if (ServletFileUpload.isMultipartContent(req)) {
uploadDispatch(req, rsp, method, uri);
} else {
doDispatch(req, rsp, method, uri);
}
}
//文件上传
private static void uploadDispatch(HttpServletRequest req, HttpServletResponse rsp, HttpMethod method, String uri) throws ServletException, IOException {
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存缓冲区,超过后写入临时文件
factory.setSizeThreshold(4096);
// 设置上传到服务器上文件的临时存放目录 -- 非常重要,防止存放到系统盘造成系统盘空间不足
factory.setRepository(new File("./uploadFileTemp"));
ServletFileUpload fileUpload = new ServletFileUpload(factory);
fileUpload.setHeaderEncoding("utf-8");
// 设置单个文件的最大上传值
fileUpload.setSizeMax(MAX_SIZE); // 文件上传上限10G
List<FileItem> fileItemList = null;
try {
fileItemList = fileUpload.parseRequest(req);
} catch (FileUploadException e) {
log.error("上传文件解析错误,{}", e.getMessage());
throw new ServletException(e);
}
/*
* 注意,在SpringMVC环境中,需要配置spring.servlet.multipart.enabled=false
* 来去掉SpringMVC对上传操作的解析,否则这里得到的上传文件个数为0
* */
if (fileItemList == null || fileItemList.size() == 0) {
throw new ServletException("没有文件");
}
List<Object> fileList = new ArrayList<>();
for (final FileItem fileItem : fileItemList) {
log.info(">>>file name:{}", fileItem.getName());
ByteArrayResource byteArr = new ByteArrayResource(fileItem.get()) {
@Override
public String getFilename() throws IllegalStateException {
return fileItem.getName();
}
};
fileList.add(byteArr);
}
// 进行转发
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
if (fileList.size() == 1) {
parts.add("file", fileList.get(0));
} else {
parts.add("file", fileList);
}
// 请求URL
if (!StringUtils.isEmpty(req.getQueryString())) {
uri = String.format(
"%s?%s",
uri,
URLDecoder.decode(req.getQueryString(), "utf-8")
);
}
// 请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.add(name, req.getHeader(name));
}
HttpEntity<MultiValueMap<String, Object>> mutiReq = new HttpEntity<>(parts, headers);
ResponseEntity<WebResponse> responseEntity = restTemplate.exchange(uri, method, mutiReq, WebResponse.class);
if (responseEntity.hasBody()) {
// 设置响应信息
rsp.setStatus(responseEntity.getStatusCodeValue());
PrintWriter out = rsp.getWriter();
out.write(JSON.toJSONString(responseEntity.getBody()));
}
}
//非文件请求
private static void doDispatch(HttpServletRequest req, HttpServletResponse rsp, HttpMethod method, String uri) throws IOException {
String requestBody = IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8);
// 请求体
Object body = null;
if (!StringUtils.isEmpty(requestBody)) {
body = JSON.parse(requestBody);
}
// 请求头
HttpHeaders headers = new HttpHeaders();
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.add(name, req.getHeader(name));
}
// 请求URL
if (!StringUtils.isEmpty(req.getQueryString())) {
uri = String.format(
"%s?%s",
uri,
URLDecoder.decode(req.getQueryString(), "utf-8")
);
}
HttpEntity<Object> httpEntity = new HttpEntity<>(body, headers);
ResponseEntity<WebResponse> exchange;
try {
// 发送请求
exchange = restTemplate.exchange(
uri,
method,
httpEntity,
WebResponse.class
);
// 设置响应信息
rsp.setStatus(exchange.getStatusCodeValue());
PrintWriter out = rsp.getWriter();
out.write(JSON.toJSONString(exchange.getBody()));
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException("请求异常,请检查服务信息");
}
}
}