最近在做一个多项目整合的工作,因为每个项目都有自己的一套网关,每个网关都有自己的加解密算法,整合到一起要求对外提供统一的用户鉴权,而且不对原有系统做大规模的重构,基于这些现实考虑使用两重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才能够通过鉴权并解析出敏感信息传递到指定的路由服务中。
针对业务网关有两种实现策略:
- 通过Feign将业务微服务的API统一封装并暴露给统一网关,这样统一网关只需要路由到业务网关即可,但是缺陷就是每次API调用会多一次业务网关的调用。
- 统一网关直接路由到业务微服务,这样业务微服务的API直接暴露给统一网关,优点就是API调用更加直接,推荐使用这个策略。
两个业务网关的授权&鉴权服务示例
- 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进行敏感数据加密
- 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鉴权后访问业务微服务的流程。