美文网首页
微服务方式实现双重网关

微服务方式实现双重网关

作者: 李征兵 | 来源:发表于2019-11-23 09:13 被阅读0次

最近在做一个多项目整合的工作,因为每个项目都有自己的一套网关,每个网关都有自己的加解密算法,整合到一起要求对外提供统一的用户鉴权,而且不对原有系统做大规模的重构,基于这些现实考虑使用两重API网关架构来构建新系统的统一网关体系。

双重网关架构

双重网关架构

备注:其中的统一网关、业务网关、业务微服务都是微服务的模式注册到微服务中心。

统一网关

这个网关采用zuul来进行网关过滤及路由,其中过滤规则由各个业务网关以微服务方式提供,通过Feign来调用,这个方式也是区别于传统网关的,也是实现双重网关的关键所在。
这里要遵循的基本原则是:授权/鉴权一体化,即授权策略和鉴权方法都是由各个业务网关自己维护,这样就确保了功能的封闭性和一致性,在开发和后期维护中都非常的方便高效。

public class AccessFilter extends ZuulFilter {
    @Autowired
    private IGatewayH5app gatewayH5app;
    @Autowired
    private IGatewayApp gatewayApp;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        ResultData resultData = new ResultData();

        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        HttpServletResponse response=ctx.getResponse();
        response.setContentType("application/json;charset=UTF-8");

        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            resultData.setRetCode(-1000);
            resultData.setRetMessage("app-token没有写入cookie!");
        }
        else {
            String accessToken = null;
            String appType = null;
            for (Cookie cookie : cookies) {
                switch (cookie.getName()) {
                    case "app-token":
                        accessToken = cookie.getValue();
                        break;
                    case "app-type":
                        appType = cookie.getValue();
                        break;
                }
            }
            if (accessToken == null) {
                log.warn("app-token is empty");
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(401);
                resultData.setRetCode(-1001);
                resultData.setRetMessage("app-token没有写入cookie!");
            } else {
                //将解码后的数据传递给微服务
                log.info("app-type:{}", appType);
                if (toonType != null) {
                    //跟进前端APP类型路由到不同的鉴权微服务逻辑
                    ResultData resultDataAuth = new ResultData();
                    switch (appType) {
                        case "app":
                            resultDataAuth=gatewayApp.auth(accessToken);
                            break;
                        case "h5app":
                            resultDataAuth=gatewayH5app.auth(accessToken);
                            break;
                    }
                    log.info("鉴权结果{}", resultDataAuth);
                    if (resultDataAuth.getRetCode() == 0) {
                        JSONObject data = resultDataAuth.getData();
                        List<String> userIdList = new ArrayList<>();
                        userIdList.add(data.getString("userId"));

                        //URL后面附带参数传递(get请求?后面参数不丢失)
                        request.getParameterMap();
                        Map<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();
                        if (requestQueryParams == null) {
                            requestQueryParams = new HashMap<>();
                        }
                        requestQueryParams.put("userId", userIdList);
                        ctx.setRequestQueryParams(requestQueryParams);
                        return null;
                    } else {
                        ctx.setSendZuulResponse(false);
                        ctx.setResponseStatusCode(403);
                        resultData.setRetCode(-1002);
                        resultData.setRetMessage("鉴权失败");
                    }
                } else {
                    ctx.setSendZuulResponse(false);
                    ctx.setResponseStatusCode(403);
                    resultData.setRetCode(-1003);
                    resultData.setRetMessage("没有设置toontype");
                }
            }
        }
        ctx.setResponseBody(JSONObject.toJSONString(resultData));
        return null;
    }
}

备注:这个类是zuul的主类实现了过滤/路由,其中的鉴权部分调用了相关的微服务,这些微服务以@Autowired的方式注入进来。
接口定义如下:

@FeignClient(name = "gateway-h5app")
public interface IGatewayH5app {
    @PostMapping("/auth")
    ResultData auth(@RequestParam String token);
}
@FeignClient(name = "gateway-app")
public interface IGatewayApp {
    @PostMapping("/auth")
    ResultData auth(@RequestParam String token);
}

zuul路由策略

路由策略通过配置实现,因为是微服务所以直接指定路由到的微服务id即可,配置文件可以存储到微服务治理中心的配置中心。

##################
# 以下配置到consul #
##################
#健康监控配置
management:
  health:
    redis:
      enabled: false
    consul:
      enabled: true
#feign配置
zuul:
  prefix: /openapi
  strip-prefix: true
  routes:
    baseuser:
      path: /userbase/**
      serviceId: user-base
    orguser:
      path: /userorg/**
      serviceId: user-org
ribbon:
  ReadTimeout: 120000
  ConnectTimeout: 300000
#链路跟踪sleuth & zipkin配置
spring:
  zipkin:
    base-url: http://172.28.43.90:9411
  sleuth:
    sampler:
      percentage: 1.0

备注:其中的user-base、user-org分别是两个业务微服务。

业务网关

这个网关集群按照业务划分,每个网关实现了授权和鉴权的策略算法,并以微服务的方式提供,其中授权是对相关敏感信息做加密并以token的方式存储到cookie中,鉴权是将存储在客户端的token通过相应的解密算法进行核验和鉴权,确保该token的合法性、有效性,只有有效的token才能够通过鉴权并解析出敏感信息传递到指定的路由服务中。

针对业务网关有两种实现策略:

  1. 通过Feign将业务微服务的API统一封装并暴露给统一网关,这样统一网关只需要路由到业务网关即可,但是缺陷就是每次API调用会多一次业务网关的调用。
  2. 统一网关直接路由到业务微服务,这样业务微服务的API直接暴露给统一网关,优点就是API调用更加直接,推荐使用这个策略。

两个业务网关的授权&鉴权服务示例

  1. h5APP业务网关
@Slf4j
@RestController
public class Controller {
    @Value("${jwtSecret}")
    private String jwtSecret;
    @Value("${tokenExpireTime}")
    private Long tokenExpireTime;

    @Autowired
    private IUserBase userBase;

    @PostMapping(value = "/auth")
    @ApiOperation(value = "鉴权", notes = "H5 APP登录用户鉴权")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "token", value = "token", dataType = "String")
    })
    public ResultData auth(@RequestParam String token) {
        ResultData resultData = new ResultData();
        //通过JWT解析token进行合法性验证
        try {
            Claims claims=Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
            JSONObject data=new JSONObject();
            data.put("userId", claims.get("userId").toString());
            resultData.setRetCode(0);
            resultData.setRetMessage("鉴权成功!");
            resultData.setData(data);
        }
        catch (ExpiredJwtException e){
            resultData.setRetCode(-1000);
            resultData.setRetMessage("token过期!");
        }
        return resultData;
    }

    @PostMapping(value = "/register")
    @ApiOperation(value = "注册", notes = "通过手机号、密码注册用户")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "mobile", value = "基础用户手机号", dataType = "String"),
            @ApiImplicitParam(paramType = "query", name = "password", value = "登录密码", dataType = "String")
    })
    public ResultData register(HttpServletResponse response, @RequestParam String mobile,@RequestParam String password) {
        ResultData resultData=userBase.register(mobile, password);
        if(resultData.getRetCode()==0) {
            response.addCookie(new Cookie("app-token", JwtUtils.createJWT(tokenExpireTime, jwtSecret, resultData.getData().getString("userId"))));
            response.addCookie(new Cookie("app-type","h5app"));
        }
        return resultData;
    }

    @PostMapping(value = "/login")
    @ApiOperation(value = "登录", notes = "手机号、密码登录")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "mobile", value = "基础用户手机号", dataType = "String"),
            @ApiImplicitParam(paramType = "query", name = "password", value = "登录密码", dataType = "String")
    })
    public ResultData login(HttpServletResponse response, @RequestParam String mobile,@RequestParam String password) {
        ResultData resultData=userBase.login(mobile, password);
        if(resultData.getRetCode()==0) {
            response.addCookie(new Cookie("app-token", JwtUtils.createJWT(tokenExpireTime, jwtSecret, resultData.getData().getString("userId"))));
            response.addCookie(new Cookie("app-type","h5app"));
        }
        return resultData;
    }
}

备注:该网关使用JWT进行敏感数据加密

  1. APP业务网关
@RestController
public class Controller {
    @Value("${publicKey}")
    private String publicKeyBase64;
    @Value("${privateKey}")
    private String privateKeyBase64;

    @Autowired
    private IUserBase userBase;

    @PostMapping(value = "/auth")
    @ApiOperation(value = "鉴权", notes = "APP登录用户鉴权")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "token", value = "token", dataType = "String")
    })
    public ResultData auth(@RequestParam String token) {
        ResultData resultData = new ResultData();
        try {
            PrivateKey privateKey = RSAUtils.getPrivateKey(privateKeyBase64);
            String userId=new String(RSAUtils.decryptByPrivateKey(Base64.getDecoder().decode(token.getBytes()), privateKey));
            JSONObject data=new JSONObject();
            data.put("userId", userId);
            resultData.setRetCode(0);
            resultData.setRetMessage("鉴权成功!");
            resultData.setData(data);
        } catch (Exception e) {
            log.error("{}",e.getLocalizedMessage());
        }
        return resultData;
    }

    @PostMapping(value = "/login")
    @ApiOperation(value = "登录", notes = "手机号、密码登录")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "mobile", value = "基础用户手机号", dataType = "String"),
            @ApiImplicitParam(paramType = "query", name = "password", value = "登录密码", dataType = "String")
    })
    public ResultData login(HttpServletResponse response, @RequestParam String mobile, @RequestParam String password){
        ResultData resultData=userBase.login(mobile, password);
        try {
            if (resultData.getRetCode() == 0) {
                PublicKey publicKey = RSAUtils.getPublicKey(publicKeyBase64);
                response.addCookie(new Cookie("app-token", new String(Base64.getEncoder().encode(RSAUtils.encryptByPublicKey(resultData.getData().getString("userId").getBytes(), publicKey)))));
                response.addCookie(new Cookie("app-type", "app"));
            }
        } catch (Exception e) {
            log.error("{}",e.getLocalizedMessage());
        }
        return resultData;
    }
}

备注:该网关使用RSA进行敏感数据加密
H5业务网关以微服务方式提供了授权/鉴权服务,其中授权服务直接暴露给客户端,客户端调用后将业务类型app_type和授权token写入cookie,鉴权服务暴露给统一网关,对传递的token进行鉴权,鉴权成功后将token中的加密信息解析出来后返回给统一网关,由统一网关路由到业务微服务并将该参数传递下去。

调用序列图

网关调用序列图

备注:其中register、login是生成授权token流程,readUserinfo是通过token鉴权后访问业务微服务的流程。

相关文章

  • 微服务方式实现双重网关

    最近在做一个多项目整合的工作,因为每个项目都有自己的一套网关,每个网关都有自己的加解密算法,整合到一起要求对外提供...

  • 微服务之聚合网关Fizz安装教程

    Fizz 网关简介 Fizz Gateway 是一个基于 Java开发的微服务网关,能够实现热服务编排、自动授权选...

  • 单例模式

    懒汉式双重检查方式 这种方式是通过双重检查+synchronized实现线程安全。volatile:防止jvm指令...

  • 2018-03-27

    微服务网关 什么是微服务网关 微服务网关也称为服务网关或者API网关。服务网关 = 路由转发 + 过滤器...

  • 微服务网关鉴权:gateway使用、网关限流使用、用户密码加密、

    目标 掌握微服务网关Gateway的系统搭建 掌握网关限流的实现 能够使用BCrypt实现对密码的加密与验证 了解...

  • spring cloud gateway的作用(面试)

    1. 什么是网关 网关是整个微服务API请求的入口,负责拦截所有请求,分发到服务上去。可以实现日志拦截、权限控制、...

  • spring-cloud-gateway动态路由

    spring cloud gateway 动态路由实现: gateway-server(网关服务) dynamic...

  • 微服务实践分享与探讨

    服务调用关系 API网关优缺点 简化沟通方式API网关对所有微服务提供单一的访问点 安全性对客户端隐藏了服务发现和...

  • 微服务 六:服务网关

    什么是服务网关、为什么需要服务网关 服务除了内部相互之间调用和通信之外,最终要以某种方式暴露出去,才能让外界系统(...

  • 微服务架构中整合网关、权限服务

    前言:之前的文章有讲过微服务的权限系列和网关实现,都是孤立存在,本文将整合后端服务与网关、权限系统。安全权限部分的...

网友评论

      本文标题:微服务方式实现双重网关

      本文链接:https://www.haomeiwen.com/subject/bnicwctx.html