最近在做一個(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)

備注:其中的統(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)策略:
- 通過(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)用。
- 統(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ù)示例
- 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ù)加密
- 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)用序列圖

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