微服务方式实现双重网关

最近在做一个多项目整合的工作,因为每个项目都有自己的一套网关,每个网关都有自己的加解密算法,整合到一起要求对外提供统一的用户鉴权,而且不对原有系统做大规模的重构,基于这些现实考虑使用两重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鉴权后访问业务微服务的流程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容