微服務(wù)方式實(shí)現(xiàn)雙重網(wǎng)關(guān)

最近在做一個(gè)多項(xiàng)目整合的工作,因?yàn)槊總€(gè)項(xiàng)目都有自己的一套網(wǎng)關(guān),每個(gè)網(wǎng)關(guān)都有自己的加解密算法,整合到一起要求對(duì)外提供統(tǒng)一的用戶鑒權(quán),而且不對(duì)原有系統(tǒng)做大規(guī)模的重構(gòu),基于這些現(xiàn)實(shí)考慮使用兩重API網(wǎng)關(guān)架構(gòu)來(lái)構(gòu)建新系統(tǒng)的統(tǒng)一網(wǎng)關(guān)體系。

雙重網(wǎng)關(guān)架構(gòu)

雙重網(wǎng)關(guān)架構(gòu)

備注:其中的統(tǒng)一網(wǎng)關(guān)、業(yè)務(wù)網(wǎng)關(guān)、業(yè)務(wù)微服務(wù)都是微服務(wù)的模式注冊(cè)到微服務(wù)中心。

統(tǒng)一網(wǎng)關(guān)

這個(gè)網(wǎng)關(guān)采用zuul來(lái)進(jìn)行網(wǎng)關(guān)過(guò)濾及路由,其中過(guò)濾規(guī)則由各個(gè)業(yè)務(wù)網(wǎng)關(guān)以微服務(wù)方式提供,通過(guò)Feign來(lái)調(diào)用,這個(gè)方式也是區(qū)別于傳統(tǒng)網(wǎng)關(guān)的,也是實(shí)現(xiàn)雙重網(wǎng)關(guān)的關(guān)鍵所在。
這里要遵循的基本原則是:授權(quán)/鑒權(quán)一體化,即授權(quán)策略和鑒權(quán)方法都是由各個(gè)業(yè)務(wù)網(wǎng)關(guān)自己維護(hù),這樣就確保了功能的封閉性和一致性,在開(kāi)發(fā)和后期維護(hù)中都非常的方便高效。

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沒(méi)有寫(xiě)入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沒(méi)有寫(xiě)入cookie!");
            } else {
                //將解碼后的數(shù)據(jù)傳遞給微服務(wù)
                log.info("app-type:{}", appType);
                if (toonType != null) {
                    //跟進(jìn)前端APP類(lèi)型路由到不同的鑒權(quán)微服務(wù)邏輯
                    ResultData resultDataAuth = new ResultData();
                    switch (appType) {
                        case "app":
                            resultDataAuth=gatewayApp.auth(accessToken);
                            break;
                        case "h5app":
                            resultDataAuth=gatewayH5app.auth(accessToken);
                            break;
                    }
                    log.info("鑒權(quán)結(jié)果{}", resultDataAuth);
                    if (resultDataAuth.getRetCode() == 0) {
                        JSONObject data = resultDataAuth.getData();
                        List<String> userIdList = new ArrayList<>();
                        userIdList.add(data.getString("userId"));

                        //URL后面附帶參數(shù)傳遞(get請(qǐng)求?后面參數(shù)不丟失)
                        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("鑒權(quán)失敗");
                    }
                } else {
                    ctx.setSendZuulResponse(false);
                    ctx.setResponseStatusCode(403);
                    resultData.setRetCode(-1003);
                    resultData.setRetMessage("沒(méi)有設(shè)置toontype");
                }
            }
        }
        ctx.setResponseBody(JSONObject.toJSONString(resultData));
        return null;
    }
}

備注:這個(gè)類(lèi)是zuul的主類(lèi)實(shí)現(xiàn)了過(guò)濾/路由,其中的鑒權(quán)部分調(diào)用了相關(guān)的微服務(wù),這些微服務(wù)以@Autowired的方式注入進(jìn)來(lái)。
接口定義如下:

@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路由策略

路由策略通過(guò)配置實(shí)現(xiàn),因?yàn)槭俏⒎?wù)所以直接指定路由到的微服務(wù)id即可,配置文件可以存儲(chǔ)到微服務(wù)治理中心的配置中心。

##################
# 以下配置到consul #
##################
#健康監(jiān)控配置
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分別是兩個(gè)業(yè)務(wù)微服務(wù)。

業(yè)務(wù)網(wǎng)關(guān)

這個(gè)網(wǎng)關(guān)集群按照業(yè)務(wù)劃分,每個(gè)網(wǎng)關(guān)實(shí)現(xiàn)了授權(quán)和鑒權(quán)的策略算法,并以微服務(wù)的方式提供,其中授權(quán)是對(duì)相關(guān)敏感信息做加密并以token的方式存儲(chǔ)到cookie中,鑒權(quán)是將存儲(chǔ)在客戶端的token通過(guò)相應(yīng)的解密算法進(jìn)行核驗(yàn)和鑒權(quán),確保該token的合法性、有效性,只有有效的token才能夠通過(guò)鑒權(quán)并解析出敏感信息傳遞到指定的路由服務(wù)中。

針對(duì)業(yè)務(wù)網(wǎng)關(guān)有兩種實(shí)現(xiàn)策略:

  1. 通過(guò)Feign將業(yè)務(wù)微服務(wù)的API統(tǒng)一封裝并暴露給統(tǒng)一網(wǎng)關(guān),這樣統(tǒng)一網(wǎng)關(guān)只需要路由到業(yè)務(wù)網(wǎng)關(guān)即可,但是缺陷就是每次API調(diào)用會(huì)多一次業(yè)務(wù)網(wǎng)關(guān)的調(diào)用。
  2. 統(tǒng)一網(wǎng)關(guān)直接路由到業(yè)務(wù)微服務(wù),這樣業(yè)務(wù)微服務(wù)的API直接暴露給統(tǒng)一網(wǎng)關(guān),優(yōu)點(diǎn)就是API調(diào)用更加直接,推薦使用這個(gè)策略。

兩個(gè)業(yè)務(wù)網(wǎng)關(guān)的授權(quán)&鑒權(quán)服務(wù)示例

  1. h5APP業(yè)務(wù)網(wǎng)關(guān)
@Slf4j
@RestController
public class Controller {
    @Value("${jwtSecret}")
    private String jwtSecret;
    @Value("${tokenExpireTime}")
    private Long tokenExpireTime;

    @Autowired
    private IUserBase userBase;

    @PostMapping(value = "/auth")
    @ApiOperation(value = "鑒權(quán)", notes = "H5 APP登錄用戶鑒權(quán)")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "token", value = "token", dataType = "String")
    })
    public ResultData auth(@RequestParam String token) {
        ResultData resultData = new ResultData();
        //通過(guò)JWT解析token進(jìn)行合法性驗(yàn)證
        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("鑒權(quán)成功!");
            resultData.setData(data);
        }
        catch (ExpiredJwtException e){
            resultData.setRetCode(-1000);
            resultData.setRetMessage("token過(guò)期!");
        }
        return resultData;
    }

    @PostMapping(value = "/register")
    @ApiOperation(value = "注冊(cè)", notes = "通過(guò)手機(jī)號(hào)、密碼注冊(cè)用戶")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "mobile", value = "基礎(chǔ)用戶手機(jī)號(hào)", 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 = "手機(jī)號(hào)、密碼登錄")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "mobile", value = "基礎(chǔ)用戶手機(jī)號(hào)", 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;
    }
}

備注:該網(wǎng)關(guān)使用JWT進(jìn)行敏感數(shù)據(jù)加密

  1. APP業(yè)務(wù)網(wǎng)關(guān)
@RestController
public class Controller {
    @Value("${publicKey}")
    private String publicKeyBase64;
    @Value("${privateKey}")
    private String privateKeyBase64;

    @Autowired
    private IUserBase userBase;

    @PostMapping(value = "/auth")
    @ApiOperation(value = "鑒權(quán)", notes = "APP登錄用戶鑒權(quán)")
    @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("鑒權(quán)成功!");
            resultData.setData(data);
        } catch (Exception e) {
            log.error("{}",e.getLocalizedMessage());
        }
        return resultData;
    }

    @PostMapping(value = "/login")
    @ApiOperation(value = "登錄", notes = "手機(jī)號(hào)、密碼登錄")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "mobile", value = "基礎(chǔ)用戶手機(jī)號(hào)", 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;
    }
}

備注:該網(wǎng)關(guān)使用RSA進(jìn)行敏感數(shù)據(jù)加密
H5業(yè)務(wù)網(wǎng)關(guān)以微服務(wù)方式提供了授權(quán)/鑒權(quán)服務(wù),其中授權(quán)服務(wù)直接暴露給客戶端,客戶端調(diào)用后將業(yè)務(wù)類(lèi)型app_type和授權(quán)token寫(xiě)入cookie,鑒權(quán)服務(wù)暴露給統(tǒng)一網(wǎng)關(guān),對(duì)傳遞的token進(jìn)行鑒權(quán),鑒權(quán)成功后將token中的加密信息解析出來(lái)后返回給統(tǒng)一網(wǎng)關(guān),由統(tǒng)一網(wǎng)關(guān)路由到業(yè)務(wù)微服務(wù)并將該參數(shù)傳遞下去。

調(diào)用序列圖

網(wǎng)關(guān)調(diào)用序列圖

備注:其中register、login是生成授權(quán)token流程,readUserinfo是通過(guò)token鑒權(quán)后訪問(wèn)業(yè)務(wù)微服務(wù)的流程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容