SpringBoot应用篇(二):手写网关服务

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("请求异常,请检查服务信息");
        }
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值